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 0000000..57cf447 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png differ 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 0000000..1a09822 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Contents.json index d77ddd3..6aa90f6 100644 --- a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Contents.json +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.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_02_selected.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@2x.png new file mode 100644 index 0000000..0c62624 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@2x.png differ 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 0000000..bbe4d51 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Contents.json b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Contents.json index d77ddd3..6aa90f6 100644 --- a/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Contents.json +++ b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.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_05.imageset/Frame@2x.png b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@2x.png new file mode 100644 index 0000000..67dad40 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@2x.png differ 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 0000000..5876884 Binary files /dev/null and b/ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png differ diff --git a/ShortPlay/Source/en.lproj/Localizable.strings b/ShortPlay/Source/en.lproj/Localizable.strings index 9de9a77..d70f758 100644 --- a/ShortPlay/Source/en.lproj/Localizable.strings +++ b/ShortPlay/Source/en.lproj/Localizable.strings @@ -9,3 +9,4 @@ "Home" = "Home"; "For You" = "For You"; "Error" = "Error"; +"Profile" = "Profile";