视频详情播放,创建历史记录,数据加密
This commit is contained in:
parent
335ede5be7
commit
0f79f340d9
@ -13,6 +13,8 @@ extension AppDelegate {
|
|||||||
// UIView.et_Awake()
|
// UIView.et_Awake()
|
||||||
tabBarConfig()
|
tabBarConfig()
|
||||||
// keyBoardStyle()
|
// keyBoardStyle()
|
||||||
|
|
||||||
|
SPToast.config()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
self.appConfig()
|
self.appConfig()
|
||||||
|
|
||||||
SPLoginManager.manager.requestVisitorLogin(completer: nil)
|
SPLoginManager.manager.requestVisitorLogin(completer: nil)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class SPNavigationController: UINavigationController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
self.jx_transitionAwake()
|
||||||
// Do any additional setup after loading the view.
|
// Do any additional setup after loading the view.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,11 +12,13 @@ class SPTabBarController: UITabBarController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.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]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ class SPViewController: UIViewController, JYPageChildContollerProtocol {
|
|||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
self.isViewDidLoad = true
|
self.isViewDidLoad = true
|
||||||
self.edgesForExtendedLayout = []
|
self.edgesForExtendedLayout = []
|
||||||
|
self.view.backgroundColor = .black
|
||||||
|
|
||||||
if let navi = navigationController {
|
if let navi = navigationController {
|
||||||
if navi.visibleViewController == self {
|
if navi.visibleViewController == self {
|
||||||
|
@ -24,4 +24,5 @@ class SPHomeAPI: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
41
ShortPlay/Base/Networking/API/SPVideoAPI.swift
Normal file
41
ShortPlay/Base/Networking/API/SPVideoAPI.swift
Normal file
@ -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<SPVideoDetailModel>) 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<String>) in
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -90,6 +90,7 @@ extension SPApi: TargetType {
|
|||||||
"system-type" : "ios",
|
"system-type" : "ios",
|
||||||
"idfa" : JXUUID.idfa(),
|
"idfa" : JXUUID.idfa(),
|
||||||
"model" : UIDevice.sp_machineModelName(),
|
"model" : UIDevice.sp_machineModelName(),
|
||||||
|
// "security" : "false",
|
||||||
]
|
]
|
||||||
//登录信息
|
//登录信息
|
||||||
dic["authorization"] = userToken
|
dic["authorization"] = userToken
|
||||||
|
173
ShortPlay/Base/Networking/Base/SPCryptService.swift
Normal file
173
ShortPlay/Base/Networking/Base/SPCryptService.swift
Normal file
@ -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..<size {
|
||||||
|
salt.append(UInt8.random(in: 0...255))
|
||||||
|
}
|
||||||
|
|
||||||
|
return salt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密数据
|
||||||
|
static func encrypt(_ data: String) -> 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)..<encodedData.count)
|
||||||
|
|
||||||
|
// 解密
|
||||||
|
guard let decryptedData = decryptWithSalt(encryptedData, salt: salt) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将解密数据转换为字符串
|
||||||
|
return String(data: decryptedData, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用盐解密数据
|
||||||
|
static func decryptWithSalt(_ 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(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..<len {
|
||||||
|
// let nextIndex = hexString.index(index, offsetBy: 2)
|
||||||
|
// let byteString = hexString[index..<nextIndex]
|
||||||
|
//
|
||||||
|
// guard let num = UInt8(byteString, radix: 16) else {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// data.append(num)
|
||||||
|
// index = nextIndex
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// self = data
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Data转换为16进制字符串
|
||||||
|
func hexEncodedString() -> String {
|
||||||
|
return map { String(format: "%02hhx", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -96,57 +96,65 @@ class SPNetwork: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let tempData: [String : Any] = try response.mapJSON() as! [String : Any]
|
let tempData = try response.mapString()
|
||||||
spLog(message: parameters.parameters)
|
spLog(message: parameters.parameters)
|
||||||
spLog(message: parameters.path)
|
spLog(message: parameters.path)
|
||||||
spLog(message: tempData as NSDictionary)
|
|
||||||
var response = SPNetworkResponse<T>.deserialize(from: tempData)
|
|
||||||
|
|
||||||
if response != nil {
|
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
let response: SPNetworkResponse<T> = _deserialize(data: tempData)
|
||||||
|
|
||||||
if response?.code == SPNetworkCodeSucceed {
|
DispatchQueue.main.async {
|
||||||
|
if response.code != SPNetworkCodeSucceed {
|
||||||
} else {
|
if parameters.isToast {
|
||||||
if parameters.isToast {
|
SPToast.show(text: response.msg)
|
||||||
SPToast.show(text: response?.msg)
|
}
|
||||||
}
|
}
|
||||||
|
completion?(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
response?.rawData = tempData
|
|
||||||
completion?(response!)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
response = SPNetworkResponse<T>()
|
|
||||||
response?.code = -1
|
|
||||||
if parameters.isToast {
|
|
||||||
SPToast.show(text: "Error".localized)
|
|
||||||
// ETHUD.showToast(text: "系统错误".localized)
|
|
||||||
}
|
|
||||||
completion?(response!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
var response = SPNetworkResponse<T>()
|
var res = SPNetworkResponse<T>()
|
||||||
response.code = -1
|
res.code = -1
|
||||||
if parameters.isToast {
|
if parameters.isToast {
|
||||||
SPToast.show(text: "Error".localized)
|
SPToast.show(text: "Error".localized)
|
||||||
// ETHUD.showToast(text: "系统错误".localized)
|
|
||||||
}
|
}
|
||||||
completion?(response)
|
completion?(res)
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
spLog(message: error)
|
spLog(message: error)
|
||||||
var response = SPNetworkResponse<T>()
|
var res = SPNetworkResponse<T>()
|
||||||
response.code = -1
|
res.code = -1
|
||||||
if parameters.isToast {
|
if parameters.isToast {
|
||||||
SPToast.show(text: "Error".localized)
|
SPToast.show(text: "Error".localized)
|
||||||
// ETHUD.showToast(text: "网络异常".localized)
|
|
||||||
}
|
}
|
||||||
completion?(response)
|
completion?(res)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
///解析数据
|
||||||
|
static private func _deserialize<T>(data: String) -> SPNetworkResponse<T> {
|
||||||
|
var response: SPNetworkResponse<T>?
|
||||||
|
|
||||||
|
let time = Date().timeIntervalSince1970
|
||||||
|
if let decrypted = SPCryptService.decrypt(data) {
|
||||||
|
spLog(message: decrypted)
|
||||||
|
response = SPNetworkResponse<T>.deserialize(from: decrypted)
|
||||||
|
response?.rawData = decrypted
|
||||||
|
}
|
||||||
|
spLog(message: Date().timeIntervalSince1970 - time)
|
||||||
|
|
||||||
|
if let response = response {
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
var response = SPNetworkResponse<T>()
|
||||||
|
response.code = -1
|
||||||
|
response.msg = "Error".localized
|
||||||
|
return response
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ class SPForYouViewController: SPPlayerListViewController {
|
|||||||
|
|
||||||
requestDataArr(page: 1)
|
requestDataArr(page: 1)
|
||||||
|
|
||||||
|
self.delegate = self
|
||||||
|
self.dataSource = self
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
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 {
|
extension SPForYouViewController {
|
||||||
|
|
||||||
private func requestDataArr(page: Int) {
|
private func requestDataArr(page: Int) {
|
||||||
|
@ -19,7 +19,7 @@ class SPHomePageController: SPViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.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 --------------
|
//MARK: -------------- JYPageControllerDelegate & JYPageControllerDataSource --------------
|
||||||
extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource {
|
extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSource {
|
||||||
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {
|
func pageController(_ pageController: JYPageController, frameForSegmentedView segmentedView: JYSegmentedView) -> CGRect {
|
||||||
|
21
ShortPlay/Class/Mine/Controller/SPMineViewController.swift
Normal file
21
ShortPlay/Class/Mine/Controller/SPMineViewController.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// SPMineViewController.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPMineViewController: SPViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -7,6 +7,32 @@
|
|||||||
|
|
||||||
import UIKit
|
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 {
|
class SPPlayerListViewController: SPViewController {
|
||||||
|
|
||||||
|
|
||||||
@ -14,11 +40,20 @@ class SPPlayerListViewController: SPViewController {
|
|||||||
return CGSize(width: kSPScreenWidth, height: kSPScreenHeight - kSPTabBarHeight)
|
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?
|
var pagination: SPListPaginationModel?
|
||||||
|
|
||||||
private var viewModel = SPPlayerListViewModel()
|
///自动下一级
|
||||||
|
var autoNextEpisode = false
|
||||||
|
|
||||||
|
private(set) var viewModel = SPPlayerListViewModel()
|
||||||
|
|
||||||
private(set) var currentIndexPath = IndexPath(row: 0, section: 0)
|
private(set) var currentIndexPath = IndexPath(row: 0, section: 0)
|
||||||
|
|
||||||
@ -30,7 +65,7 @@ class SPPlayerListViewController: SPViewController {
|
|||||||
return layout
|
return layout
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var collectionView: SPCollectionView = {
|
private(set) lazy var collectionView: SPCollectionView = {
|
||||||
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
collectionView.dataSource = self
|
collectionView.dataSource = self
|
||||||
@ -39,10 +74,14 @@ class SPPlayerListViewController: SPViewController {
|
|||||||
collectionView.showsHorizontalScrollIndicator = false
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
collectionView.bounces = false
|
collectionView.bounces = false
|
||||||
collectionView.scrollsToTop = false
|
collectionView.scrollsToTop = false
|
||||||
SPPlayerListCell.registerCell(collectionView: collectionView)
|
PlayerCellClass.registerCell(collectionView: collectionView)
|
||||||
return collectionView
|
return collectionView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
@ -55,6 +94,7 @@ class SPPlayerListViewController: SPViewController {
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
|
||||||
|
|
||||||
sp_setupUI()
|
sp_setupUI()
|
||||||
|
sp_addActio()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
@ -63,7 +103,7 @@ class SPPlayerListViewController: SPViewController {
|
|||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
if !self.dataArr.isEmpty && self.viewModel.isPlaying {
|
if getDataCount() > 0 && self.viewModel.isPlaying {
|
||||||
self.viewModel.currentPlayer?.start()
|
self.viewModel.currentPlayer?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,9 +117,7 @@ class SPPlayerListViewController: SPViewController {
|
|||||||
func setDataArr(dataArr: [Any]) {
|
func setDataArr(dataArr: [Any]) {
|
||||||
self.dataArr = dataArr
|
self.dataArr = dataArr
|
||||||
|
|
||||||
CATransaction.begin()
|
reloadData()
|
||||||
self.collectionView.reloadData()
|
|
||||||
CATransaction.commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -97,14 +135,24 @@ class SPPlayerListViewController: SPViewController {
|
|||||||
|
|
||||||
self.viewModel.isPlaying = true
|
self.viewModel.isPlaying = true
|
||||||
|
|
||||||
if self.dataArr.count - currentIndexPath.row <= 2 {
|
if getDataCount() - currentIndexPath.row <= 2 {
|
||||||
// self.loadMoreData()
|
self.loadMoreData()
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentIndexPath.row <= 2 {
|
if currentIndexPath.row <= 2 {
|
||||||
// self.loadUpMoreData()
|
// self.loadUpMoreData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reloadData() {
|
||||||
|
CATransaction.begin()
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDataCount() -> Int {
|
||||||
|
return self.collectionView(self.collectionView, numberOfItemsInSection: 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SPPlayerListViewController {
|
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 --------------
|
//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
|
||||||
extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
let cell = SPPlayerListCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
|
var cell: UICollectionViewCell = PlayerCellClass.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
|
||||||
cell.model = dataArr[indexPath.row]
|
|
||||||
|
|
||||||
if self.viewModel.currentPlayer == nil, indexPath == currentIndexPath {
|
if let newCell = self.dataSource?.sp_playerListViewController(self, collectionView, cellForItemAt: indexPath, oldCell: cell) {
|
||||||
self.currentIndexPath = indexPath
|
cell = newCell
|
||||||
self.viewModel.currentPlayer = cell
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
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) {
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
scrollDidEnd(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
|
scrollDidEnd(scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scrollDidEnd(_ scrollView: UIScrollView) {
|
||||||
let offsetY = scrollView.contentOffset.y
|
let offsetY = scrollView.contentOffset.y
|
||||||
let indexPaths = self.collectionView.indexPathsForVisibleItems
|
let indexPaths = self.collectionView.indexPathsForVisibleItems
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
@ -164,7 +299,7 @@ extension SPPlayerListViewController: UICollectionViewDelegate, UICollectionView
|
|||||||
extension SPPlayerListViewController {
|
extension SPPlayerListViewController {
|
||||||
|
|
||||||
@objc func didBecomeActiveNotification() {
|
@objc func didBecomeActiveNotification() {
|
||||||
if !self.dataArr.isEmpty && self.viewModel.isPlaying && isDidAppear {
|
if getDataCount() > 0 && self.viewModel.isPlaying && isDidAppear {
|
||||||
self.viewModel.currentPlayer?.start()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,12 @@ protocol SPPlayerProtocol: NSObjectProtocol {
|
|||||||
var playerFinishHadle: (() -> Void)? { get set }
|
var playerFinishHadle: (() -> Void)? { get set }
|
||||||
|
|
||||||
var model: Any? { get set }
|
var model: Any? { get set }
|
||||||
|
var videoInfo: SPVideoInfoModel? { get set }
|
||||||
|
|
||||||
var isCurrent: Bool { get set }
|
var isCurrent: Bool { get set }
|
||||||
|
|
||||||
|
var rate: Float { get set }
|
||||||
|
|
||||||
///播放准备
|
///播放准备
|
||||||
func prepare()
|
func prepare()
|
||||||
|
|
||||||
@ -24,5 +27,8 @@ protocol SPPlayerProtocol: NSObjectProtocol {
|
|||||||
|
|
||||||
///暂停播放
|
///暂停播放
|
||||||
func pause()
|
func pause()
|
||||||
|
|
||||||
|
///从头播放
|
||||||
|
func replay()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
41
ShortPlay/Class/Player/Model/SPSpeedModel.swift
Normal file
41
ShortPlay/Class/Player/Model/SPSpeedModel.swift
Normal file
@ -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
|
||||||
|
|
||||||
|
}
|
24
ShortPlay/Class/Player/Model/SPVideoDetailModel.swift
Normal file
24
ShortPlay/Class/Player/Model/SPVideoDetailModel.swift
Normal file
@ -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?
|
||||||
|
}
|
@ -9,6 +9,21 @@ import UIKit
|
|||||||
|
|
||||||
class SPPlayerControlView: UIView {
|
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)?
|
var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)?
|
||||||
|
|
||||||
@ -18,6 +33,12 @@ class SPPlayerControlView: UIView {
|
|||||||
progressView.progress = progress
|
progressView.progress = progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isCurrent: Bool = false {
|
||||||
|
didSet {
|
||||||
|
updatePlayIconState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private(set) lazy var progressView: SPPlayerProgressView = {
|
private(set) lazy var progressView: SPPlayerProgressView = {
|
||||||
let view = SPPlayerProgressView()
|
let view = SPPlayerProgressView()
|
||||||
@ -37,12 +58,33 @@ class SPPlayerControlView: UIView {
|
|||||||
return view
|
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) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(hadlePlayAndOrPaused))
|
||||||
|
self.addGestureRecognizer(tap)
|
||||||
|
|
||||||
|
|
||||||
sp_setupUI()
|
sp_setupUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "isPlaying" {
|
||||||
|
updatePlayIconState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
@ -53,6 +95,7 @@ extension SPPlayerControlView {
|
|||||||
|
|
||||||
private func sp_setupUI() {
|
private func sp_setupUI() {
|
||||||
addSubview(progressView)
|
addSubview(progressView)
|
||||||
|
addSubview(playImageView)
|
||||||
|
|
||||||
progressView.snp.makeConstraints { make in
|
progressView.snp.makeConstraints { make in
|
||||||
make.left.equalToSuperview().offset(10)
|
make.left.equalToSuperview().offset(10)
|
||||||
@ -60,7 +103,35 @@ extension SPPlayerControlView {
|
|||||||
make.bottom.equalToSuperview().offset(-20)
|
make.bottom.equalToSuperview().offset(-20)
|
||||||
make.height.equalTo(30)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,18 @@ import UIKit
|
|||||||
|
|
||||||
class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
||||||
|
|
||||||
|
weak var viewModel: SPPlayerListViewModel? {
|
||||||
|
didSet {
|
||||||
|
controlView.viewModel = viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLoop = true {
|
||||||
|
didSet {
|
||||||
|
player.isLoop = isLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private lazy var player: SPPlayer = {
|
private lazy var player: SPPlayer = {
|
||||||
let player = SPPlayer()
|
let player = SPPlayer()
|
||||||
player.playerView = playerView
|
player.playerView = playerView
|
||||||
@ -48,16 +58,44 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//MARK: SPPlayerProtocol
|
//MARK: SPPlayerProtocol
|
||||||
var model: Any? {
|
var model: Any? {
|
||||||
didSet {
|
didSet {
|
||||||
guard let model = model as? SPShortModel else { return }
|
self.controlView.progress = 0
|
||||||
player.setPlayUrl(url: model.video_info?.video_url ?? "")
|
self.coverImageView.isHidden = false
|
||||||
coverImageView.sp_setImage(url: model.image_url)
|
|
||||||
|
if let model = model as? SPShortModel {
|
||||||
|
self.controlView.model = model
|
||||||
|
coverImageView.sp_setImage(url: model.image_url)
|
||||||
|
} else if let model = model as? SPVideoDetailModel {
|
||||||
|
self.controlView.model = model.shortPlayInfo
|
||||||
|
coverImageView.sp_setImage(url: model.shortPlayInfo?.image_url)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var 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)?
|
var playerFinishHadle: (() -> Void)?
|
||||||
@ -74,13 +112,17 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
|||||||
player.pause()
|
player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func replay() {
|
||||||
|
player.replay()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SPPlayerListCell {
|
extension SPPlayerListCell {
|
||||||
|
|
||||||
private func sp_setupUI() {
|
private func sp_setupUI() {
|
||||||
contentView.addSubview(coverImageView)
|
|
||||||
contentView.addSubview(playerView)
|
contentView.addSubview(playerView)
|
||||||
|
contentView.addSubview(coverImageView)
|
||||||
contentView.addSubview(controlView)
|
contentView.addSubview(controlView)
|
||||||
|
|
||||||
coverImageView.snp.makeConstraints { make in
|
coverImageView.snp.makeConstraints { make in
|
||||||
@ -103,7 +145,7 @@ extension SPPlayerListCell {
|
|||||||
extension SPPlayerListCell: SPPlayerDelegate {
|
extension SPPlayerListCell: SPPlayerDelegate {
|
||||||
|
|
||||||
func sp_playCompletion(_ player: SPPlayer) {
|
func sp_playCompletion(_ player: SPPlayer) {
|
||||||
|
self.playerFinishHadle?()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sp_playLoadingEnd(_ player: SPPlayer) {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
12
ShortPlay/Class/Player/View/SPTVPlayerListCell.swift
Normal file
12
ShortPlay/Class/Player/View/SPTVPlayerListCell.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// SPTVPlayerListCell.swift
|
||||||
|
// ShortPlay
|
||||||
|
//
|
||||||
|
// Created by 曾觉新 on 2025/4/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SPTVPlayerListCell: SPPlayerListCell {
|
||||||
|
|
||||||
|
}
|
@ -9,7 +9,7 @@ import UIKit
|
|||||||
|
|
||||||
class SPPlayerListViewModel: NSObject {
|
class SPPlayerListViewModel: NSObject {
|
||||||
|
|
||||||
var isPlaying = false
|
@objc dynamic var isPlaying: Bool = true
|
||||||
|
|
||||||
private var _currentPlayer: SPPlayerProtocol?
|
private var _currentPlayer: SPPlayerProtocol?
|
||||||
var currentPlayer: SPPlayerProtocol? {
|
var currentPlayer: SPPlayerProtocol? {
|
||||||
@ -18,6 +18,9 @@ class SPPlayerListViewModel: NSObject {
|
|||||||
_currentPlayer?.pause()
|
_currentPlayer?.pause()
|
||||||
|
|
||||||
_currentPlayer = newValue
|
_currentPlayer = newValue
|
||||||
|
_currentPlayer?.playerFinishHadle = { [weak self] in
|
||||||
|
self?.handlePlayFinish?()
|
||||||
|
}
|
||||||
_currentPlayer?.isCurrent = true
|
_currentPlayer?.isCurrent = true
|
||||||
}
|
}
|
||||||
get {
|
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)?
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,16 @@ import UIKit
|
|||||||
|
|
||||||
class SPToast: NSObject {
|
class SPToast: NSObject {
|
||||||
|
|
||||||
|
static func config() {
|
||||||
|
CSToastManager.setTapToDismissEnabled(false)
|
||||||
|
CSToastManager.setDefaultDuration(2)
|
||||||
|
CSToastManager.setDefaultPosition(CSToastPositionCenter)
|
||||||
|
}
|
||||||
|
|
||||||
static func show(text: String?) {
|
static func show(text: String?) {
|
||||||
guard let text = text else { return }
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@ import ZFPlayer
|
|||||||
// ///更新当前进度
|
// ///更新当前进度
|
||||||
// @objc optional func sp_onCurrentPositionUpdate(_ player: SPPlayer, position: Int)
|
// @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)
|
@objc optional func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int)
|
||||||
|
|
||||||
@ -26,14 +29,27 @@ import ZFPlayer
|
|||||||
|
|
||||||
///缓冲完成
|
///缓冲完成
|
||||||
@objc optional func sp_playLoadingEnd(_ player: SPPlayer)
|
@objc optional func sp_playLoadingEnd(_ player: SPPlayer)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SPPlayer: NSObject {
|
class SPPlayer: NSObject {
|
||||||
|
|
||||||
|
@objc enum PlayState: Int {
|
||||||
|
case unknown
|
||||||
|
case playing
|
||||||
|
case paused
|
||||||
|
case failed
|
||||||
|
case stopped
|
||||||
|
}
|
||||||
|
|
||||||
weak var delegate: SPPlayerDelegate?
|
weak var delegate: SPPlayerDelegate?
|
||||||
|
|
||||||
private(set) lazy var isPlaying = false
|
private(set) lazy var isPlaying = false
|
||||||
|
private(set) lazy var playState: PlayState = .unknown
|
||||||
|
|
||||||
|
/**
|
||||||
|
是否添加息屏监控
|
||||||
|
*/
|
||||||
|
private var isAddIdleTimerDisabledObserver = false
|
||||||
|
|
||||||
///总进度
|
///总进度
|
||||||
var duration: Int {
|
var duration: Int {
|
||||||
@ -44,6 +60,16 @@ class SPPlayer: NSObject {
|
|||||||
return Int(self.player.currentTime)
|
return Int(self.player.currentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///0.5 - 2
|
||||||
|
var rate: Float {
|
||||||
|
set {
|
||||||
|
player.rate = newValue
|
||||||
|
}
|
||||||
|
get {
|
||||||
|
return player.rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var playerView: UIView? {
|
var playerView: UIView? {
|
||||||
didSet {
|
didSet {
|
||||||
playerView?.addSubview(player.view)
|
playerView?.addSubview(player.view)
|
||||||
@ -55,50 +81,99 @@ class SPPlayer: NSObject {
|
|||||||
|
|
||||||
private lazy var player: ZFAVPlayerManager = {
|
private lazy var player: ZFAVPlayerManager = {
|
||||||
let player = ZFAVPlayerManager()
|
let player = ZFAVPlayerManager()
|
||||||
|
player.shouldAutoPlay = false
|
||||||
return player
|
return player
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var isLoop = true
|
var isLoop = true
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.stop()
|
||||||
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
player.scalingMode = .aspectFill
|
player.scalingMode = .aspectFill
|
||||||
sp_addAction()
|
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) {
|
func setPlayUrl(url: String) {
|
||||||
let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url))
|
let proxyURL = KTVHTTPCache.proxyURL(withOriginalURL: URL(string: url))
|
||||||
self.player.assetURL = proxyURL
|
self.player.assetURL = proxyURL
|
||||||
|
// self.prepare()
|
||||||
// self.player.assetURL = URL(string: url)
|
|
||||||
self.prepare()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///准备播放
|
///准备播放
|
||||||
func prepare() {
|
func prepare() {
|
||||||
self.player.prepareToPlay()
|
// self.player.prepareToPlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
self.isPlaying = false
|
self.isPlaying = false
|
||||||
player.stop()
|
player.stop()
|
||||||
|
|
||||||
|
self.removeIdleTimerDisabledObserver()
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
self.isPlaying = true
|
self.isPlaying = true
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = true
|
||||||
|
self.addIdleTimerDisabledObserver()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///暂停
|
///暂停
|
||||||
func pause() {
|
func pause() {
|
||||||
self.isPlaying = false
|
self.isPlaying = false
|
||||||
player.pause()
|
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) {
|
func seekToTime(toTime: Int) {
|
||||||
// self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE)
|
// self.player.seek(toTime: Int64(toTime), seekMode: AVP_SEEKMODE_ACCURATE)
|
||||||
self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil)
|
self.player.seek(toTime: TimeInterval(toTime), completionHandler: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SPPlayer {
|
extension SPPlayer {
|
||||||
@ -115,7 +190,25 @@ extension SPPlayer {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if playState == .playStatePlaying, !isPlaying {
|
if playState == .playStatePlaying, !isPlaying {
|
||||||
self.pause()
|
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)")
|
spLog(message: "播放状态====\(playState)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,8 +217,21 @@ extension SPPlayer {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if loadState == .playable, !isPlaying {
|
if loadState == .playable, !isPlaying {
|
||||||
self.pause()
|
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 {
|
if isLoop {
|
||||||
self.player.replay()
|
self.player.replay()
|
||||||
} else {
|
} else {
|
||||||
|
self.isPlaying = false
|
||||||
self.delegate?.sp_playCompletion?(self)
|
self.delegate?.sp_playCompletion?(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,12 @@
|
|||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 957 B |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_01.imageset/Frame@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
@ -5,10 +5,12 @@
|
|||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@2x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_02_selected.imageset/Frame@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -5,10 +5,12 @@
|
|||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Frame@2x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "Frame@3x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
|
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@2x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png
vendored
Normal file
BIN
ShortPlay/Source/Assets.xcassets/TabBar/tabbar_icon_05.imageset/Frame@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
@ -9,3 +9,4 @@
|
|||||||
"Home" = "Home";
|
"Home" = "Home";
|
||||||
"For You" = "For You";
|
"For You" = "For You";
|
||||||
"Error" = "Error";
|
"Error" = "Error";
|
||||||
|
"Profile" = "Profile";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user