From 0f79f340d9a500fe9e27edc237592405aec4ad41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E8=A7=89=E6=96=B0?= Date: Thu, 10 Apr 2025 18:20:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E8=AF=A6=E6=83=85=E6=92=AD?= =?UTF-8?q?=E6=94=BE=EF=BC=8C=E5=88=9B=E5=BB=BA=E5=8E=86=E5=8F=B2=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=EF=BC=8C=E6=95=B0=E6=8D=AE=E5=8A=A0=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppDelegate/AppDelegate+Config.swift | 2 + ShortPlay/AppDelegate/AppDelegate.swift | 1 + .../Controller/SPNavigationController.swift | 2 +- .../Base/Controller/SPTabBarController.swift | 8 +- .../Base/Controller/SPViewController.swift | 1 + ShortPlay/Base/Networking/API/SPHomeAPI.swift | 1 + .../Base/Networking/API/SPVideoAPI.swift | 41 ++++ ShortPlay/Base/Networking/Base/SPApi.swift | 1 + .../Base/Networking/Base/SPCryptService.swift | 173 ++++++++++++++++ .../Base/Networking/Base/SPNetwork.swift | 68 ++++--- .../Controller/SPForYouViewController.swift | 34 ++++ .../Controller/SPHomePageController.swift | 14 +- .../Controller/SPMineViewController.swift | 21 ++ .../SPPlayerListViewController.swift | 186 ++++++++++++++++-- .../SPTVPlayerListViewController.swift | 83 ++++++++ .../Class/Player/Model/SPPlayerProtocol.swift | 6 + .../Class/Player/Model/SPSpeedModel.swift | 41 ++++ .../Player/Model/SPVideoDetailModel.swift | 24 +++ .../Player/View/SPPlayerControlView.swift | 71 +++++++ .../Class/Player/View/SPPlayerListCell.swift | 62 +++++- .../Player/View/SPTVPlayerListCell.swift | 12 ++ .../ViewModel/SPPlayerListViewModel.swift | 19 +- ShortPlay/Libs/HUD/SPToast.swift | 9 +- ShortPlay/Libs/Player/SPPlayer.swift | 119 ++++++++++- .../tabbar_icon_01.imageset/Contents.json | 2 + .../tabbar_icon_01.imageset/Frame@2x.png | Bin 0 -> 957 bytes .../tabbar_icon_01.imageset/Frame@3x.png | Bin 0 -> 1831 bytes .../Contents.json | 2 + .../Frame@2x.png | Bin 0 -> 1111 bytes .../Frame@3x.png | Bin 0 -> 2100 bytes .../tabbar_icon_05.imageset/Contents.json | 2 + .../tabbar_icon_05.imageset/Frame@2x.png | Bin 0 -> 1193 bytes .../tabbar_icon_05.imageset/Frame@3x.png | Bin 0 -> 2372 bytes ShortPlay/Source/en.lproj/Localizable.strings | 1 + 34 files changed, 939 insertions(+), 67 deletions(-) create mode 100644 ShortPlay/Base/Networking/API/SPVideoAPI.swift create mode 100644 ShortPlay/Base/Networking/Base/SPCryptService.swift create mode 100644 ShortPlay/Class/Mine/Controller/SPMineViewController.swift create mode 100644 ShortPlay/Class/Player/Controller/SPTVPlayerListViewController.swift create mode 100644 ShortPlay/Class/Player/Model/SPSpeedModel.swift create mode 100644 ShortPlay/Class/Player/Model/SPVideoDetailModel.swift create mode 100644 ShortPlay/Class/Player/View/SPTVPlayerListCell.swift create mode 100644 ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png create mode 100644 ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png create mode 100644 ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@2x.png create mode 100644 ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png create mode 100644 ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@2x.png create mode 100644 ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png diff --git a/ShortPlay/AppDelegate/AppDelegate+Config.swift b/ShortPlay/AppDelegate/AppDelegate+Config.swift index e25c953..b0a41df 100644 --- a/ShortPlay/AppDelegate/AppDelegate+Config.swift +++ b/ShortPlay/AppDelegate/AppDelegate+Config.swift @@ -13,6 +13,8 @@ extension AppDelegate { // UIView.et_Awake() tabBarConfig() // keyBoardStyle() + + SPToast.config() } diff --git a/ShortPlay/AppDelegate/AppDelegate.swift b/ShortPlay/AppDelegate/AppDelegate.swift index 3759846..7a0747e 100644 --- a/ShortPlay/AppDelegate/AppDelegate.swift +++ b/ShortPlay/AppDelegate/AppDelegate.swift @@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.appConfig() SPLoginManager.manager.requestVisitorLogin(completer: nil) + return true } diff --git a/ShortPlay/Base/Controller/SPNavigationController.swift b/ShortPlay/Base/Controller/SPNavigationController.swift index d7bdcdc..5e4805b 100644 --- a/ShortPlay/Base/Controller/SPNavigationController.swift +++ b/ShortPlay/Base/Controller/SPNavigationController.swift @@ -11,7 +11,7 @@ class SPNavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() - + self.jx_transitionAwake() // Do any additional setup after loading the view. } diff --git a/ShortPlay/Base/Controller/SPTabBarController.swift b/ShortPlay/Base/Controller/SPTabBarController.swift index bbfdd4e..77d08b9 100644 --- a/ShortPlay/Base/Controller/SPTabBarController.swift +++ b/ShortPlay/Base/Controller/SPTabBarController.swift @@ -12,11 +12,13 @@ class SPTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() - let nav1 = createNavigationController(viewController: SPHomePageController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected")) + let nav1 = createNavigationController(viewController: SPHomePageController(), title: "Home".localized, image: UIImage(named: "tabbar_icon_01"), selectedImage: UIImage(named: "tabbar_icon_01_selected")) - let nav2 = createNavigationController(viewController: SPForYouViewController(), title: "For You".localized, image: UIImage(named: "tabbar_icon_01_selected"), selectedImage: UIImage(named: "tabbar_icon_01_selected")) + let nav2 = createNavigationController(viewController: SPForYouViewController(), title: "For You".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected")) - self.viewControllers = [nav1, nav2] + let nav5 = createNavigationController(viewController: SPMineViewController(), title: "Profile".localized, image: UIImage(named: "tabbar_icon_05"), selectedImage: UIImage(named: "tabbar_icon_05_selected")) + + self.viewControllers = [nav1, nav2, nav5] } diff --git a/ShortPlay/Base/Controller/SPViewController.swift b/ShortPlay/Base/Controller/SPViewController.swift index 7873c02..936869a 100644 --- a/ShortPlay/Base/Controller/SPViewController.swift +++ b/ShortPlay/Base/Controller/SPViewController.swift @@ -27,6 +27,7 @@ class SPViewController: UIViewController, JYPageChildContollerProtocol { super.viewDidLoad() self.isViewDidLoad = true self.edgesForExtendedLayout = [] + self.view.backgroundColor = .black if let navi = navigationController { if navi.visibleViewController == self { diff --git a/ShortPlay/Base/Networking/API/SPHomeAPI.swift b/ShortPlay/Base/Networking/API/SPHomeAPI.swift index 5ee9098..b13bc16 100644 --- a/ShortPlay/Base/Networking/API/SPHomeAPI.swift +++ b/ShortPlay/Base/Networking/API/SPHomeAPI.swift @@ -24,4 +24,5 @@ class SPHomeAPI: NSObject { } } + } diff --git a/ShortPlay/Base/Networking/API/SPVideoAPI.swift b/ShortPlay/Base/Networking/API/SPVideoAPI.swift new file mode 100644 index 0000000..ab80bcb --- /dev/null +++ b/ShortPlay/Base/Networking/API/SPVideoAPI.swift @@ -0,0 +1,41 @@ +// +// SPVideoAPI.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/10. +// + +import UIKit + +class SPVideoAPI: NSObject { + + ///获取视频详情 + static func requestVideoDetail(videoId: String, shortPlayId: String, completer: ((_ model: SPVideoDetailModel?) -> Void)?) { + + var param = SPNetworkParameters(path: "/getVideoDetails") + param.method = .get + param.parameters = [ + "video_id" : videoId, + "short_play_id" : shortPlayId + ] + + SPNetwork.request(parameters: param) { (response: SPNetworkResponse) in + completer?(response.data) + } + } + + ///创建播放记录 + static func requestRequestVideoPlayHistory(videoId: String, shortPlayId: String) { + var param = SPNetworkParameters(path: "/createHistory") + param.isLoding = false + param.isToast = false + param.parameters = [ + "video_id" : videoId, + "short_play_id" : shortPlayId + ] + + SPNetwork.request(parameters: param) { (response: SPNetworkResponse) in + + } + } +} diff --git a/ShortPlay/Base/Networking/Base/SPApi.swift b/ShortPlay/Base/Networking/Base/SPApi.swift index 338b893..dcd77b8 100644 --- a/ShortPlay/Base/Networking/Base/SPApi.swift +++ b/ShortPlay/Base/Networking/Base/SPApi.swift @@ -90,6 +90,7 @@ extension SPApi: TargetType { "system-type" : "ios", "idfa" : JXUUID.idfa(), "model" : UIDevice.sp_machineModelName(), +// "security" : "false", ] //登录信息 dic["authorization"] = userToken diff --git a/ShortPlay/Base/Networking/Base/SPCryptService.swift b/ShortPlay/Base/Networking/Base/SPCryptService.swift new file mode 100644 index 0000000..1ce9bff --- /dev/null +++ b/ShortPlay/Base/Networking/Base/SPCryptService.swift @@ -0,0 +1,173 @@ +// +// SPCryptService.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/10. +// + +import UIKit + +class SPCryptService: NSObject { + + static let BF_SIZE = 2048 + static let EN_STR_TAG = "$" + + // 生成随机盐 + static func randSalt() -> Data { + let size = Int.random(in: 16...64) // 随机盐的长度 + var salt = Data(capacity: size) + + for _ in 0.. String? { + guard !data.isEmpty else { + return data + } + + // 确保数据是UTF-8编码 + guard let utf8Data = data.data(using: .utf8) else { + return nil + } + + // 随机盐 + let salt = randSalt() + let saltLen = UInt8(salt.count) + + // 加密数据 + guard let encryptedData = encryptWithSalt(utf8Data, salt: salt) else { + return nil + } + + // 构建完整加密数据: [盐长度(1字节)][盐][加密数据] + var fullEncryptedData = Data() + fullEncryptedData.append(saltLen) + fullEncryptedData.append(salt) + fullEncryptedData.append(encryptedData) + + // 返回加密后的数据(16进制表示) + return EN_STR_TAG + fullEncryptedData.hexEncodedString() + } + + // 使用盐加密数据 + static func encryptWithSalt(_ data: Data, salt: Data) -> Data? { + var result = Data(capacity: data.count) + let saltLen = salt.count + + for (i, byte) in data.enumerated() { + let saltByte = salt[i % saltLen] + result.append(calSalt(v: byte, s: saltByte)) + } + + return result + } + + // 解密数据 + static func decrypt(_ data: String) -> String? { + guard !data.isEmpty else { + return data + } + + // 检查是否是加密字符串 如果不是加密串,返回原始字符串 + guard data.hasPrefix(EN_STR_TAG) else { + print("Invalid encoded string.") + return data + } + + // 提取16进制数据并解码 + let hexString = String(data.dropFirst(EN_STR_TAG.count)) + + guard let encodedData = NSData(hexString: hexString) as? Data else { +// guard let encodedData = Data(hexString: hexString) else { + print("Invalid hex string.") + return nil + } + + guard encodedData.count > 0 else { + print("Empty encoded data.") + return nil + } + + // 解析盐长度和盐 + let saltLen = Int(encodedData[0]) + guard encodedData.count > saltLen + 1 else { + print("Invalid encoded data format.") + return nil + } + + let salt = encodedData.subdata(in: 1..<(1+saltLen)) + let encryptedData = encodedData.subdata(in: (1+saltLen).. Data? { + var result = Data(capacity: data.count) + let saltLen = salt.count + for (i, byte) in data.enumerated() { + let saltByte = salt[i % saltLen] + result.append(calRemoveSalt(v: byte, s: saltByte)) + } + + return result + } + + // 计算加盐值 + private static func calSalt(v: UInt8, s: UInt8) -> UInt8 { + let r = 255 - v + if s > r { + return s - r - 1 + } + return v + s + } + + // 计算去盐值 + private static func calRemoveSalt(v: UInt8, s: UInt8) -> UInt8 { + if v >= s { + return v - s + } + return 255 - (s - v) + 1 + } +} + +// Data 扩展,用于16进制编码/解码 +extension Data { + // 16进制字符串转换为Data +// init?(hexString: String) { +// let len = hexString.count / 2 +// var data = Data(capacity: len) +// +// var index = hexString.startIndex +// for _ in 0.. String { + return map { String(format: "%02hhx", $0) }.joined() + } + +} diff --git a/ShortPlay/Base/Networking/Base/SPNetwork.swift b/ShortPlay/Base/Networking/Base/SPNetwork.swift index 1816b8a..ec514c5 100644 --- a/ShortPlay/Base/Networking/Base/SPNetwork.swift +++ b/ShortPlay/Base/Networking/Base/SPNetwork.swift @@ -96,57 +96,65 @@ class SPNetwork: NSObject { } do { - let tempData: [String : Any] = try response.mapJSON() as! [String : Any] + let tempData = try response.mapString() spLog(message: parameters.parameters) spLog(message: parameters.path) - spLog(message: tempData as NSDictionary) - var response = SPNetworkResponse.deserialize(from: tempData) - if response != nil { + + DispatchQueue.global().async { + let response: SPNetworkResponse = _deserialize(data: tempData) - if response?.code == SPNetworkCodeSucceed { - - } else { - if parameters.isToast { - SPToast.show(text: response?.msg) + DispatchQueue.main.async { + if response.code != SPNetworkCodeSucceed { + if parameters.isToast { + SPToast.show(text: response.msg) + } } + completion?(response) } - - response?.rawData = tempData - completion?(response!) - - } else { - response = SPNetworkResponse() - response?.code = -1 - if parameters.isToast { - SPToast.show(text: "Error".localized) -// ETHUD.showToast(text: "系统错误".localized) - } - completion?(response!) } + } catch { - var response = SPNetworkResponse() - response.code = -1 + var res = SPNetworkResponse() + res.code = -1 if parameters.isToast { SPToast.show(text: "Error".localized) -// ETHUD.showToast(text: "系统错误".localized) } - completion?(response) + completion?(res) } case .failure(let error): spLog(message: error) - var response = SPNetworkResponse() - response.code = -1 + var res = SPNetworkResponse() + res.code = -1 if parameters.isToast { SPToast.show(text: "Error".localized) -// ETHUD.showToast(text: "网络异常".localized) } - completion?(response) + completion?(res) break } + + } + + ///解析数据 + static private func _deserialize(data: String) -> SPNetworkResponse { + var response: SPNetworkResponse? + let time = Date().timeIntervalSince1970 + if let decrypted = SPCryptService.decrypt(data) { + spLog(message: decrypted) + response = SPNetworkResponse.deserialize(from: decrypted) + response?.rawData = decrypted + } + spLog(message: Date().timeIntervalSince1970 - time) - + if let response = response { + return response + } else { + var response = SPNetworkResponse() + response.code = -1 + response.msg = "Error".localized + return response + } } } diff --git a/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift b/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift index 636894e..ee4bee3 100644 --- a/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift +++ b/ShortPlay/Class/ForYou/Controller/SPForYouViewController.swift @@ -14,6 +14,8 @@ class SPForYouViewController: SPPlayerListViewController { requestDataArr(page: 1) + self.delegate = self + self.dataSource = self } override func viewWillAppear(_ animated: Bool) { @@ -24,6 +26,38 @@ class SPForYouViewController: SPPlayerListViewController { } +//MARK: -------------- SPPlayerListViewControllerDelegate -------------- +extension SPForYouViewController: SPPlayerListViewControllerDelegate { + func sp_playerViewControllerLoadMoreData(playerViewController: SPPlayerListViewController) { + 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: -------------- SPPlayerListViewControllerDataSource -------------- +extension SPForYouViewController: SPPlayerListViewControllerDataSource { + + func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + + if let cell = oldCell as? SPPlayerListCell { + if let model = dataArr[indexPath.row] as? SPShortModel { + cell.model = model + cell.videoInfo = model.video_info + } + } + return oldCell + } + + func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { + return oldNumber + } +} + extension SPForYouViewController { private func requestDataArr(page: Int) { diff --git a/ShortPlay/Class/Home/Controller/SPHomePageController.swift b/ShortPlay/Class/Home/Controller/SPHomePageController.swift index 5627976..f5c85e5 100644 --- a/ShortPlay/Class/Home/Controller/SPHomePageController.swift +++ b/ShortPlay/Class/Home/Controller/SPHomePageController.swift @@ -19,7 +19,7 @@ class SPHomePageController: SPViewController { override func viewDidLoad() { super.viewDidLoad() - + sp_setupUI() } @@ -30,6 +30,18 @@ class SPHomePageController: SPViewController { } +extension SPHomePageController { + + private func sp_setupUI() { + addChild(pageView) + view.addSubview(pageView.view) + + pageView.view.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} + //MARK: -------------- JYPageControllerDelegate & JYPageControllerDataSource -------------- extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource { func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect { diff --git a/ShortPlay/Class/Mine/Controller/SPMineViewController.swift b/ShortPlay/Class/Mine/Controller/SPMineViewController.swift new file mode 100644 index 0000000..84696af --- /dev/null +++ b/ShortPlay/Class/Mine/Controller/SPMineViewController.swift @@ -0,0 +1,21 @@ +// +// SPMineViewController.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/10. +// + +import UIKit + +class SPMineViewController: SPViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + + } + + + + +} diff --git a/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift b/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift index ed05023..909fe7f 100644 --- a/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift +++ b/ShortPlay/Class/Player/Controller/SPPlayerListViewController.swift @@ -7,6 +7,32 @@ import UIKit +@objc protocol SPPlayerListViewControllerDelegate { + + ///加载新数据 + @objc optional func sp_playerViewControllerLoadNewDataV2(playerViewController: SPPlayerListViewController) + + ///将要加载更多数据 + @objc optional func sp_playerViewControllerShouldLoadMoreData(playerViewController: SPPlayerListViewController) -> Bool + ///加载更多数据 + @objc optional func sp_playerViewControllerLoadMoreData(playerViewController: SPPlayerListViewController) + ///向上加载更多数据 + @objc optional func sp_playerViewControllerLoadUpMoreData(playerViewController: SPPlayerListViewController) + + ///新页面展示完成 +// @objc optional func yd_playerViewController(playerListViewController: BCListPlayerViewController, didShowPlayerPage playerViewController: YDBasePlayerViewController) +} + +@objc protocol SPPlayerListViewControllerDataSource { + + + func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell + + func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int + + +} + class SPPlayerListViewController: SPViewController { @@ -14,11 +40,20 @@ class SPPlayerListViewController: SPViewController { return CGSize(width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight) } + var PlayerCellClass: SPPlayerListCell.Type { + return SPPlayerListCell.self + } - private var dataArr: [Any] = [] + weak var delegate: SPPlayerListViewControllerDelegate? + weak var dataSource: SPPlayerListViewControllerDataSource? + + private(set) var dataArr: [Any] = [] var pagination: SPListPaginationModel? - private var viewModel = SPPlayerListViewModel() + ///自动下一级 + var autoNextEpisode = false + + private(set) var viewModel = SPPlayerListViewModel() private(set) var currentIndexPath = IndexPath(row: 0, section: 0) @@ -30,7 +65,7 @@ class SPPlayerListViewController: SPViewController { return layout }() - private lazy var collectionView: SPCollectionView = { + private(set) lazy var collectionView: SPCollectionView = { let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) collectionView.delegate = self collectionView.dataSource = self @@ -39,10 +74,14 @@ class SPPlayerListViewController: SPViewController { collectionView.showsHorizontalScrollIndicator = false collectionView.bounces = false collectionView.scrollsToTop = false - SPPlayerListCell.registerCell(collectionView: collectionView) + PlayerCellClass.registerCell(collectionView: collectionView) return collectionView }() + deinit { + NotificationCenter.default.removeObserver(self) + } + override func viewDidLoad() { super.viewDidLoad() @@ -55,6 +94,7 @@ class SPPlayerListViewController: SPViewController { NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil) sp_setupUI() + sp_addActio() } override func viewWillAppear(_ animated: Bool) { @@ -63,7 +103,7 @@ class SPPlayerListViewController: SPViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if !self.dataArr.isEmpty && self.viewModel.isPlaying { + if getDataCount() > 0 && self.viewModel.isPlaying { self.viewModel.currentPlayer?.start() } } @@ -77,9 +117,7 @@ class SPPlayerListViewController: SPViewController { func setDataArr(dataArr: [Any]) { self.dataArr = dataArr - CATransaction.begin() - self.collectionView.reloadData() - CATransaction.commit() + reloadData() } @@ -97,14 +135,24 @@ class SPPlayerListViewController: SPViewController { self.viewModel.isPlaying = true - if self.dataArr.count - currentIndexPath.row <= 2 { -// self.loadMoreData() + if getDataCount() - currentIndexPath.row <= 2 { + self.loadMoreData() } if currentIndexPath.row <= 2 { // self.loadUpMoreData() } } + + func reloadData() { + CATransaction.begin() + self.collectionView.reloadData() + CATransaction.commit() + } + + func getDataCount() -> Int { + return self.collectionView(self.collectionView, numberOfItemsInSection: 0) + } } extension SPPlayerListViewController { @@ -116,28 +164,115 @@ extension SPPlayerListViewController { } } + private func sp_addActio() { + self.viewModel.handlePauseOrPlay = { [weak self] in + self?.clickPauseOrPlay() + } + + self.viewModel.handlePlayFinish = { [weak self] in + self?.currentPlayFinish() + } + } + +} + +extension SPPlayerListViewController { + ///点击播放或暂停 + private func clickPauseOrPlay() { +// let model = self.dataArr[currentIndexPath.section] + ///打开支付页面 +// if self.onVideoPay() { +// return +// } + + if self.viewModel.isPlaying { + self.viewModel.isPlaying = false + self.viewModel.currentPlayer?.pause() + } else { + self.viewModel.isPlaying = true + self.viewModel.currentPlayer?.start() + } + } + + ///当前播放完成 + private func currentPlayFinish() { + guard self.autoNextEpisode else { return } + + scrollToNextEpisode() + } + + ///滑动至下一级 + private func scrollToNextEpisode() { + + var contentOffset = self.collectionView.contentOffset + + if hasNextEpisode() { + contentOffset.y = 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 + } } //MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource -------------- extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = SPPlayerListCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath) - cell.model = dataArr[indexPath.row] + var cell: UICollectionViewCell = PlayerCellClass.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath) - if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath { - self.currentIndexPath = indexPath - self.viewModel.currentPlayer = cell + if let newCell = self.dataSource?.sp_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) { + cell = newCell } + + if let cell = cell as? SPPlayerListCell { + if cell.viewModel == nil { + cell.viewModel = viewModel + } +// let model = dataArr[indexPath.row] +// cell.model = model + + } + + if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath, let playerProtocol = cell as? SPPlayerProtocol { + self.currentIndexPath = indexPath + self.viewModel.currentPlayer = playerProtocol + } + return cell } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return dataArr.count + let count = dataArr.count + + if let newCount = self.dataSource?.sp_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 { @@ -164,7 +299,7 @@ extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionView extension SPPlayerListViewController { @objc func didBecomeActiveNotification() { - if !self.dataArr.isEmpty && self.viewModel.isPlaying && isDidAppear { + if getDataCount() > 0 && self.viewModel.isPlaying && isDidAppear { self.viewModel.currentPlayer?.start() } } @@ -175,3 +310,20 @@ extension SPPlayerListViewController { } + +extension SPPlayerListViewController { + + private func loadMoreData() { + let isLoad = self.delegate?.sp_playerViewControllerShouldLoadMoreData?(playerViewController: self) + if isLoad != false { + self.delegate?.sp_playerViewControllerLoadMoreData?(playerViewController: self) + } + } + + private func loadUpMoreData() { +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in +// guard let self = self else { return } +// } + self.delegate?.sp_playerViewControllerLoadUpMoreData?(playerViewController: self) + } +} diff --git a/ShortPlay/Class/Player/Controller/SPTVPlayerListViewController.swift b/ShortPlay/Class/Player/Controller/SPTVPlayerListViewController.swift new file mode 100644 index 0000000..f1b4aa8 --- /dev/null +++ b/ShortPlay/Class/Player/Controller/SPTVPlayerListViewController.swift @@ -0,0 +1,83 @@ +// +// SPTVPlayerListViewController.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/10. +// + +import UIKit + +class SPTVPlayerListViewController: SPPlayerListViewController { + + override var PlayerCellClass: SPPlayerListCell.Type { + return SPTVPlayerListCell.self + } + + override var contentSize: CGSize { + return CGSize(width: kSPScreenWidth, height: kSPScreenHeight) + } + + + var videoId: String? + var shortPlayId: String? + + private var detailModel: SPVideoDetailModel? + + override func viewDidLoad() { + super.viewDidLoad() + self.autoNextEpisode = true + self.dataSource = self + + requestDetailData() + } + + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + + } + + override func play() { + super.play() + if let _ = self.viewModel.currentPlayer?.model as? SPVideoDetailModel, + let videoInfo = self.viewModel.currentPlayer?.videoInfo + { + SPVideoAPI.requestRequestVideoPlayHistory(videoId: videoInfo.short_play_video_id ?? "", shortPlayId: videoInfo.short_play_id ?? "") + } + } + +} + +//MARK: -------------- SPPlayerListViewControllerDataSource -------------- +extension SPTVPlayerListViewController: SPPlayerListViewControllerDataSource { + func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell { + if let cell = oldCell as? SPPlayerListCell { + cell.model = detailModel + cell.videoInfo = detailModel?.episodeList?[indexPath.row] + cell.isLoop = false + } + return oldCell + } + + func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, numberOfItemsInSection section: Int, oldNumber: Int) -> Int { + return detailModel?.episodeList?.count ?? 0 + } +} + + +extension SPTVPlayerListViewController { + + private func requestDetailData() { + guard let videoId = self.videoId, let shortPlayId = self.shortPlayId else { return } + + SPVideoAPI.requestVideoDetail(videoId: videoId, shortPlayId: shortPlayId) { [weak self] model in + guard let self = self else { return } + if let model = model { + self.detailModel = model + self.reloadData() + self.play() + } + } + } +} diff --git a/ShortPlay/Class/Player/Model/SPPlayerProtocol.swift b/ShortPlay/Class/Player/Model/SPPlayerProtocol.swift index 164af56..26227a4 100644 --- a/ShortPlay/Class/Player/Model/SPPlayerProtocol.swift +++ b/ShortPlay/Class/Player/Model/SPPlayerProtocol.swift @@ -13,9 +13,12 @@ protocol SPPlayerProtocol: NSObjectProtocol { var playerFinishHadle: (() -> Void)? { get set } var model: Any? { get set } + var videoInfo: SPVideoInfoModel? { get set } var isCurrent: Bool { get set } + var rate: Float { get set } + ///播放准备 func prepare() @@ -24,5 +27,8 @@ protocol SPPlayerProtocol: NSObjectProtocol { ///暂停播放 func pause() + + ///从头播放 + func replay() } diff --git a/ShortPlay/Class/Player/Model/SPSpeedModel.swift b/ShortPlay/Class/Player/Model/SPSpeedModel.swift new file mode 100644 index 0000000..9c88883 --- /dev/null +++ b/ShortPlay/Class/Player/Model/SPSpeedModel.swift @@ -0,0 +1,41 @@ +// +// SPSpeedModel.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/10. +// + +import UIKit + +struct SPSpeedModel { + + enum Speed: String { + case x0_75 = "0.75x" + case x1 = "1.0x" + case x1_25 = "1.25x" + case x1_5 = "1.5x" + case x2 = "2.0x" + + func getRate() -> Float { + switch self { + case .x0_75: + return 0.75 + + case .x1: + return 1 + + case .x1_25: + return 1.25 + + case .x1_5: + return 1.5 + + case .x2: + return 2 + } + } + } + + var speed: Speed = .x1 + +} diff --git a/ShortPlay/Class/Player/Model/SPVideoDetailModel.swift b/ShortPlay/Class/Player/Model/SPVideoDetailModel.swift new file mode 100644 index 0000000..9ce2125 --- /dev/null +++ b/ShortPlay/Class/Player/Model/SPVideoDetailModel.swift @@ -0,0 +1,24 @@ +// +// SPVideoDetailModel.swift +// ShortPlay +// +// Created by 曾觉新 on 2025/4/10. +// + +import UIKit +import SmartCodable + +class SPVideoDetailModel: SPModel, SmartCodable { + + var business_model: String? + var video_info: SPVideoInfoModel? + var shortPlayInfo: SPShortModel? + var episodeList: [SPVideoInfoModel]? + 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/ShortPlay/Class/Player/View/SPPlayerControlView.swift b/ShortPlay/Class/Player/View/SPPlayerControlView.swift index b305ef4..fe3aa95 100644 --- a/ShortPlay/Class/Player/View/SPPlayerControlView.swift +++ b/ShortPlay/Class/Player/View/SPPlayerControlView.swift @@ -9,6 +9,21 @@ import UIKit class SPPlayerControlView: UIView { + weak var viewModel: SPPlayerListViewModel? { + didSet { + viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil) + } + } + + var model: Any? { + didSet { + guard let model = model as? SPShortModel else { return } + + + + } + } + ///滑动进度条 var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)? @@ -18,6 +33,12 @@ class SPPlayerControlView: UIView { progressView.progress = progress } } + + var isCurrent: Bool = false { + didSet { + updatePlayIconState() + } + } private(set) lazy var progressView: SPPlayerProgressView = { let view = SPPlayerProgressView() @@ -37,12 +58,33 @@ class SPPlayerControlView: UIView { return view }() + private lazy var playImageView: UIImageView = { + let imageView = UIImageView() + imageView.backgroundColor = .red + imageView.isHidden = true + return imageView + }() + + deinit { + viewModel?.removeObserver(self, forKeyPath: "isPlaying") + } + override init(frame: CGRect) { super.init(frame: frame) + let tap = UITapGestureRecognizer(target: self, action: #selector(hadlePlayAndOrPaused)) + self.addGestureRecognizer(tap) + + sp_setupUI() } + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "isPlaying" { + updatePlayIconState() + } + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -53,6 +95,7 @@ extension SPPlayerControlView { private func sp_setupUI() { addSubview(progressView) + addSubview(playImageView) progressView.snp.makeConstraints { make in make.left.equalToSuperview().offset(10) @@ -60,7 +103,35 @@ extension SPPlayerControlView { make.bottom.equalToSuperview().offset(-20) make.height.equalTo(30) } + + playImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.height.equalTo(100) + } } +} + +extension SPPlayerControlView { + + private func updatePlayIconState() { + let isPlaying = self.viewModel?.isPlaying ?? false + if isCurrent { + playImageView.isHidden = isPlaying + } else { + playImageView.isHidden = true + } + } + + @objc private func hadlePlayAndOrPaused() { +// self.viewModel?.handlePauseOrPlay?() + + guard let model = model as? SPShortModel else { return } + let vc = SPTVPlayerListViewController() + vc.shortPlayId = model.short_play_id + vc.videoId = model.video_info?.short_play_video_id + SPAPPTool.topViewController()?.navigationController?.pushViewController(vc, animated: true) + } + } diff --git a/ShortPlay/Class/Player/View/SPPlayerListCell.swift b/ShortPlay/Class/Player/View/SPPlayerListCell.swift index 73f67b2..ea6b46a 100644 --- a/ShortPlay/Class/Player/View/SPPlayerListCell.swift +++ b/ShortPlay/Class/Player/View/SPPlayerListCell.swift @@ -9,8 +9,18 @@ import UIKit class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol { + weak var viewModel: SPPlayerListViewModel? { + didSet { + controlView.viewModel = viewModel + } + } + + var isLoop = true { + didSet { + player.isLoop = isLoop + } + } - private lazy var player: SPPlayer = { let player = SPPlayer() player.playerView = playerView @@ -48,16 +58,44 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol { fatalError("init(coder:) has not been implemented") } + //MARK: SPPlayerProtocol var model: Any? { didSet { - guard let model = model as? SPShortModel else { return } - player.setPlayUrl(url: model.video_info?.video_url ?? "") - coverImageView.sp_setImage(url: model.image_url) + 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) + } + } } - var isCurrent: Bool = false + var videoInfo: SPVideoInfoModel? { + didSet { + player.setPlayUrl(url: videoInfo?.video_url ?? "") + } + } + + var isCurrent: Bool = false { + didSet { + controlView.isCurrent = isCurrent + } + } + + var rate: Float { + set { + return player.rate = newValue + } + get { + return player.rate + } + } ///播放完成 var playerFinishHadle: (() -> Void)? @@ -74,13 +112,17 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol { player.pause() } + func replay() { + player.replay() + } + } extension SPPlayerListCell { private func sp_setupUI() { - contentView.addSubview(coverImageView) contentView.addSubview(playerView) + contentView.addSubview(coverImageView) contentView.addSubview(controlView) coverImageView.snp.makeConstraints { make in @@ -103,7 +145,7 @@ extension SPPlayerListCell { extension SPPlayerListCell: SPPlayerDelegate { func sp_playCompletion(_ player: SPPlayer) { - + self.playerFinishHadle?() } func sp_playLoadingEnd(_ player: SPPlayer) { @@ -118,4 +160,10 @@ extension SPPlayerListCell: SPPlayerDelegate { } + func sp_player(_ player: SPPlayer, playStateDidChanged state: SPPlayer.PlayState) { + if state == .playing { + self.coverImageView.isHidden = true + } + } + } diff --git a/ShortPlay/Class/Player/View/SPTVPlayerListCell.swift b/ShortPlay/Class/Player/View/SPTVPlayerListCell.swift new file mode 100644 index 0000000..8084c20 --- /dev/null +++ b/ShortPlay/Class/Player/View/SPTVPlayerListCell.swift @@ -0,0 +1,12 @@ +// +// 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 40da0b1..a2812c8 100644 --- a/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift +++ b/ShortPlay/Class/Player/ViewModel/SPPlayerListViewModel.swift @@ -9,7 +9,7 @@ import UIKit class SPPlayerListViewModel: NSObject { - var isPlaying = false + @objc dynamic var isPlaying: Bool = true private var _currentPlayer: SPPlayerProtocol? var currentPlayer: SPPlayerProtocol? { @@ -18,6 +18,9 @@ class SPPlayerListViewModel: NSObject { _currentPlayer?.pause() _currentPlayer = newValue + _currentPlayer?.playerFinishHadle = { [weak self] in + self?.handlePlayFinish?() + } _currentPlayer?.isCurrent = true } get { @@ -25,5 +28,19 @@ class SPPlayerListViewModel: NSObject { } } + private(set) var speed: SPSpeedModel.Speed = .x1 + + ///设置倍速播放 + func setSpeedPlay(speed: SPSpeedModel.Speed) { + self.speed = speed + currentPlayer?.rate = speed.getRate() + } + + + ///点暂停或播放 + var handlePauseOrPlay: (() -> Void)? + ///播放完成 + var handlePlayFinish: (() -> Void)? + } diff --git a/ShortPlay/Libs/HUD/SPToast.swift b/ShortPlay/Libs/HUD/SPToast.swift index 83a74d3..8d86f5d 100644 --- a/ShortPlay/Libs/HUD/SPToast.swift +++ b/ShortPlay/Libs/HUD/SPToast.swift @@ -9,9 +9,16 @@ import UIKit class SPToast: NSObject { + static func config() { + CSToastManager.setTapToDismissEnabled(false) + CSToastManager.setDefaultDuration(2) + CSToastManager.setDefaultPosition(CSToastPositionCenter) + } + static func show(text: String?) { guard let text = text else { return } - SPAPPTool.getKeyWindow()?.makeToast(text, duration: 2, position: nil) +// SPAPPTool.getKeyWindow()?.makeToast(text, duration: 2, position: nil) + SPAPPTool.getKeyWindow()?.makeToast(text) } } diff --git a/ShortPlay/Libs/Player/SPPlayer.swift b/ShortPlay/Libs/Player/SPPlayer.swift index d5a16c2..fb89ef2 100644 --- a/ShortPlay/Libs/Player/SPPlayer.swift +++ b/ShortPlay/Libs/Player/SPPlayer.swift @@ -15,6 +15,9 @@ import ZFPlayer // ///更新当前进度 // @objc optional func sp_onCurrentPositionUpdate(_ player: SPPlayer, position: Int) + ///播放状态变化 + @objc optional func sp_player(_ player: SPPlayer, playStateDidChanged state: SPPlayer.PlayState) + ///播放时间发生变化 @objc optional func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int) @@ -26,14 +29,27 @@ import ZFPlayer ///缓冲完成 @objc optional func sp_playLoadingEnd(_ player: SPPlayer) - } class SPPlayer: NSObject { + @objc enum PlayState: Int { + case unknown + case playing + case paused + case failed + case stopped + } + weak var delegate: SPPlayerDelegate? private(set) lazy var isPlaying = false + private(set) lazy var playState: PlayState = .unknown + + /** + 是否添加息屏监控 + */ + private var isAddIdleTimerDisabledObserver = false ///总进度 var duration: Int { @@ -44,6 +60,16 @@ class SPPlayer: NSObject { return Int(self.player.currentTime) } + ///0.5 - 2 + var rate: Float { + set { + player.rate = newValue + } + get { + return player.rate + } + } + var playerView: UIView? { didSet { playerView?.addSubview(player.view) @@ -55,50 +81,99 @@ class SPPlayer: NSObject { private lazy var player: ZFAVPlayerManager = { let player = ZFAVPlayerManager() + player.shouldAutoPlay = false return player }() var isLoop = true + deinit { + self.stop() + } + override init() { super.init() player.scalingMode = .aspectFill sp_addAction() } + /** + 添加息屏监控 + */ + private func addIdleTimerDisabledObserver() { + if !isAddIdleTimerDisabledObserver { + isAddIdleTimerDisabledObserver = true + UIApplication.shared.addObserver(self, forKeyPath: "idleTimerDisabled", options: NSKeyValueObservingOptions.new, context: nil) + } + } + /** + 删除息屏监控 + */ + private func removeIdleTimerDisabledObserver() { + if isAddIdleTimerDisabledObserver { + isAddIdleTimerDisabledObserver = false + UIApplication.shared.removeObserver(self, forKeyPath: "idleTimerDisabled") + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if !UIApplication.shared.isIdleTimerDisabled { + UIApplication.shared.isIdleTimerDisabled = true + } + } + func setPlayUrl(url: String) { let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url)) self.player.assetURL = proxyURL - -// self.player.assetURL = URL(string: url) - self.prepare() +// self.prepare() } ///准备播放 func prepare() { - self.player.prepareToPlay() +// self.player.prepareToPlay() } func stop() { self.isPlaying = false player.stop() + + self.removeIdleTimerDisabledObserver() + UIApplication.shared.isIdleTimerDisabled = false } func start() { self.isPlaying = true player.play() + + UIApplication.shared.isIdleTimerDisabled = true + self.addIdleTimerDisabledObserver() + } ///暂停 func pause() { self.isPlaying = false player.pause() + + self.removeIdleTimerDisabledObserver() + UIApplication.shared.isIdleTimerDisabled = false + } + + ///从头播放 + func replay() { + self.isPlaying = true + self.player.replay() + + UIApplication.shared.isIdleTimerDisabled = true + self.addIdleTimerDisabledObserver() } func seekToTime(toTime: Int) { // self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE) self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil) } + + } extension SPPlayer { @@ -115,7 +190,25 @@ extension SPPlayer { guard let self = self else { return } if playState == .playStatePlaying, !isPlaying { self.pause() + } else if playState == .playStatePaused, isPlaying { + self.start() } + switch playState { + case .playStateUnknown: + self.playState = .unknown + case .playStatePlaying: + self.playState = .playing + case .playStatePaused: + self.playState = .paused + case .playStatePlayStopped: + self.playState = .stopped + case .playStatePlayFailed: + self.playState = .failed + + default: + self.playState = .unknown + } + self.delegate?.sp_player?(self, playStateDidChanged: self.playState) spLog(message: "播放状态====\(playState)") } @@ -124,8 +217,21 @@ extension SPPlayer { guard let self = self else { return } if loadState == .playable, !isPlaying { self.pause() + } else if loadState == .playable, isPlaying, self.player.playState != .playStatePlaying { + self.start() + } + switch loadState { + case .prepare: + spLog(message: "加载状态====准备完成") + case .playable: + spLog(message: "加载状态====可播放") + case .playthroughOK: + spLog(message: "加载状态====将自动播放") + case .stalled: + spLog(message: "加载状态====如果已启动,将自动暂停") + default: + break } - spLog(message: "加载状态====\(loadState)") } //错误信息 @@ -139,6 +245,7 @@ extension SPPlayer { if isLoop { self.player.replay() } else { + self.isPlaying = false self.delegate?.sp_playCompletion?(self) } } diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json index d77ddd3..6aa90f6 100644 --- a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Contents.json @@ -5,10 +5,12 @@ "scale" : "1x" }, { + "filename" : "Frame@2x.png", "idiom" : "universal", "scale" : "2x" }, { + "filename" : "Frame@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..57cf447bf46e6673a906e602c3794b2431810fce GIT binary patch literal 957 zcmV;u148_XP)Px&cS%G+RA@u(m_2AzK@i9P^Dan)2oWp<1qJQ2v(Q38(Lzu=#Rw52S||o=6fKh7 zoxK-`*hY+KFrtEmq9B5u3W5qYf_4fKQVIqX{JMLio4hA$$lZH)_l^tl7OuISH#@(X z+1;5X{3nC{6Alz0i!G8xM2d*C$pdXqEvMBASapm zdtN}ffea0m*Of}u2LLvD#=ZUdQw}Uy;y~#B{JBb{YwyU&(8oMrasg3_4-&~W06m^j z|9HJdv|Ot7TdVK23eyruY}`cvgPtMIxIt_6#6rf^*gs={oNXo5ng$8OaLun@l{)~o zcm}^Om4f|BsWe3`f``fwrFajK+ydb4h-BO)!ck+)*DPrg zNAo6t{hs_xoNAA<=+VOtJTP*sh?_30XfXf(^+(90*Q~-=KxlEN*@Dq zh?!@yER-grwO#?xbpU%k+39Mvv^@-mKcs!12}Eo2!rRmp5gM{qUr1Bl%)C;3j!2A` zYEPNXjx=4VAn~UB7AvYmQLw9AR?pHDw=%C3?Res7T<7!QEbk z>?+#_MA4Nj#bC!%E3O1wTk9Ye8m>m$$IPyeuQ@snq7)w?lF52T#y!XFMGd00xeDNT zY~&abW?Nnnobvu3XErAnJBTaAXP<*aXdrpRV&)@J^fywI%;snTvVaG2FB-KrMMQi@ zcl^{^v=K4J^oZa~(jmGw_s=Uz%{N;j=}X$;I-C8q2)XB^TBDmt!lXk4WNF$Znz~2a zN?MyI0Jb+;0oDO&L{&aL`y7Z~50Had-G~J_fK31RA=^erc`9w)Oc9A;dPM?vYx{3c zb@fM4aHm|Jd-Jl^ZmL$J{xpMiYC%}^+Y~1NLB+nC0BrS%Ja3>Eh17xq)i?u fQmajlZCdSbP82^;Z}<*v00000NkvXXu0mjfeG92$ literal 0 HcmV?d00001 diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1a098225a6d48fd829c3cca3d2dc635df015982b GIT binary patch literal 1831 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

rL$JQ}o@cc9Bt?q3HnCC54;u zzXaImr_r%)=Z0-vjA_9-6zVu_CM)(&rAwxR(Ie=9keI9iAhi?Pz5+QM4TSkk%#e`q zj<)|166kteNq0fZzJNE;Ys9LvurOJ^(XO;Se)R1j90UBa!h)p*m5W=j6Ox=t? dLu?JP>A#*qQb6CJhZq0=002ovPDHLkV1k?k4om<5 literal 0 HcmV?d00001 diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..bbe4d513ac4892181c2d96e145bf155ab1b1724a GIT binary patch literal 2100 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 Hu0mjf*oY|} literal 0 HcmV?d00001 diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..58768840c3cbf9996feca8a0bb108ec58c3f1efa GIT binary patch literal 2372 zcmV-K3A^@*P)Px-{z*hZRCr$Pn}2LpR~g5@&wbmuz6{0&&e(}=DoQ4G#)-5>3#1#7a@7ipY)vP$LBuhyyra61ED(3NPwz*&P<>RXo3i-0;&Q^c8F9X zs2V}l2uenv8bQhURnMSm1XUv_8G&;#f|{F~>!wYccG=L-&}<^-W&pDR%mQ#}(O!C0 zM2-{5ZvZ@#%hew4?|=P7S6A0Ssx-<7KdWcZ)~#D>j~zQU7obIedZ}3Uym|AaQ>Xem0W=cPGyqP8PWmG9I)ELHL!Q?9-xaQp zH9d7eixw@)G&D4P*!TShMQ}|SD@}x)B69ixDMdz&h)g1)$>k!92>bH+d{aY1!~Vv` z#(XT#BQ8oEkW%UvfYt?POxUdd0N4Y_^8o%rM6YBrnUlF(PKf9t5xIhht`fm@g?aM; zTsX$3mjGGA%uhu0HP*CL0V$qUe%ZrnI^ zaB#2zAO+xtG4px=S;Wk{V|gE8QEGtN+uN@k9?tC|q7Rkq5RsEa*wowG+g0f=lKJJE zGG$7$2);o?7mxZTe+6(0GariJZ^ctm0o2*qIk~@o;C_IX0+I=$=9J^m-6HZRGY?j5 zX(95IQWuIicLM&p;CwPyf;=EU$Y$$SwY0RHiX-<-Yf=HEl)7F7&k#{v2@Vlf0N6g> zc8cnlQp#Kl7K>m@36h8m5aC8<{#{f#QRi6)RCIuz9X&717xuUG_V%_^x~;}@BG_)7 zfU}|$q2{0mqn6z1|l(kFI0p zJyF(2onsx4Qv4Jk3qwYC9XxpOo}D{)TD`BBUpTGxI)K)MbUn$;w?%b6$~>!pIyyRL zed7ZnYZdzrb}ma*mJXsuZU?ua&# zflS6(sg!!ymQJgHT$h_22c05P8vweUnIE-fY&kiu>n|2!o~O*)&&se3Xbq8clu{XvL!VYk{nCo1 z;|K`@0^p0#TF+A(Z0WQLNGY`npv?d-2)5YQnfcqcj4dZeDYXos(wQ?PLU((f>b9lR zHlQy8^u5p_{V+2xw`J@&fHnfuQv4g8%;){(9Ub;kAhrR0g2;J7MBWiNbDUYfGhIOX zaN&*jf#3{!l?V%%`H!}AS_M>m#rN+mgwj&eAQb8WDHeGnKZ}BkR#^3WG-*9QXJ!fHK~u_Qu_JPIii@^v-vTs18Qw;ol#R$ z`_Gb*L6U44vw5T08?MXqiDY}|)^4tfGnx4%ds+bNfQ)fk>un&rnfV`aosY6EbwI|5;OzJgAh#CYYLlWEHr^Z&`7aUu7?5UWK9NLLl(ne?TDx}b z2OX#OBLLp-IC95mIyQ=VZ=EB8mx<(uBF+KdAKu^5(P5q%sg2YCwY7P394Frb&>RuD zB&d;1)anw+Q4tXUeMIyIAbSgo^m$WRRZ2}KqGi4>Ye%b}k^GK5sn{KY2MTGH^~~H8 zTTq#W)*>hwG# z$Tn@-6u)mM5g^k=mdoW$PtLmniw^mI{}`4Hi2N%wtRxUH?Nm)F$Pn#|?LgKcICiC^$MW%>nTjYNR7);|(K zW1!iz@vda%ZLut|u&B7@FTNMRS3=?*XVy#NicbVcDQy}k&3_>QN0wQW6p(5BF1@dg`F?&udwcu7xW?F6$IJ_TUrZEF4dmsRb@+}{k(#7{ zLf3gZN+dViZndk(n_}7NlOotbL?(wcGMII2KVvU6ij~r*0g1?Kjzg;o`QlXim7{_Z zk&lUB6A@i