播放页面功能开发,我的页面开发
@ -14,7 +14,7 @@ class SPTabBarController: UITabBarController {
|
||||
|
||||
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_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected"))
|
||||
let nav2 = createNavigationController(viewController: SPExploreViewController(), title: "For You".localized, image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected"))
|
||||
|
||||
let nav5 = createNavigationController(viewController: SPMineViewController(), title: "Profile".localized, image: UIImage(named: "tabbar_icon_05"), selectedImage: UIImage(named: "tabbar_icon_05_selected"))
|
||||
|
||||
|
30
ShortPlay/Base/Extension/Int+SPAdd.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// Int+SPAdd.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class Int_SPAdd: NSObject {
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension Int {
|
||||
|
||||
func formatTimeGroup() -> (String, String, String) {
|
||||
let seconds = self
|
||||
|
||||
var s: String = "00"
|
||||
var m: String = "00"
|
||||
var h: String = "00"
|
||||
s = String(format: "%02d", Int(Int(seconds) % 60))
|
||||
m = String(format: "%02d", Int(seconds / 60) % 60)
|
||||
h = String(format: "%02d", Int(seconds / 3600))
|
||||
|
||||
return (h, m, s)
|
||||
}
|
||||
|
||||
}
|
@ -64,6 +64,16 @@ extension UIColor {
|
||||
return color(hex: 0xF56490, alpha: alpha)
|
||||
}
|
||||
|
||||
static func color9D9D9D(alpha: CGFloat = 1) -> UIColor {
|
||||
return color(hex: 0x9D9D9D, alpha: alpha)
|
||||
}
|
||||
|
||||
static func color545454(alpha: CGFloat = 1) -> UIColor {
|
||||
return color(hex: 0x545454, alpha: alpha)
|
||||
}
|
||||
|
||||
static func colorD568D2(alpha: CGFloat = 1) -> UIColor {
|
||||
return color(hex: 0xD568D2, alpha: alpha)
|
||||
}
|
||||
}
|
||||
|
||||
|
21
ShortPlay/Base/View/SPScrollView.swift
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// SPScrollView.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPScrollView: UIScrollView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ import UIKit
|
||||
|
||||
class SPTableView: UITableView {
|
||||
|
||||
var insetGroupedMargins: CGFloat = 12
|
||||
var insetGroupedMargins: CGFloat = 15
|
||||
|
||||
override init(frame: CGRect, style: UITableView.Style) {
|
||||
super.init(frame: frame, style: style)
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// SPForYouViewController.swift
|
||||
// SPExploreViewController.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/9.
|
||||
@ -7,7 +7,11 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPForYouViewController: SPPlayerListViewController {
|
||||
class SPExploreViewController: SPPlayerListViewController {
|
||||
|
||||
override var PlayerCellClass: SPPlayerListCell.Type {
|
||||
return SPExplorePlayerCell.self
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@ -27,7 +31,7 @@ class SPForYouViewController: SPPlayerListViewController {
|
||||
}
|
||||
|
||||
//MARK: -------------- SPPlayerListViewControllerDelegate --------------
|
||||
extension SPForYouViewController: SPPlayerListViewControllerDelegate {
|
||||
extension SPExploreViewController: SPPlayerListViewControllerDelegate {
|
||||
func sp_playerViewControllerLoadMoreData(playerViewController: SPPlayerListViewController) {
|
||||
guard let pagination = self.pagination else { return }
|
||||
guard let page = self.pagination?.current_page else { return }
|
||||
@ -40,7 +44,7 @@ extension SPForYouViewController: SPPlayerListViewControllerDelegate {
|
||||
}
|
||||
|
||||
//MARK: -------------- SPPlayerListViewControllerDataSource --------------
|
||||
extension SPForYouViewController: SPPlayerListViewControllerDataSource {
|
||||
extension SPExploreViewController: SPPlayerListViewControllerDataSource {
|
||||
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath, oldCell: UICollectionViewCell) -> UICollectionViewCell {
|
||||
|
||||
@ -58,7 +62,7 @@ extension SPForYouViewController: SPPlayerListViewControllerDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
extension SPForYouViewController {
|
||||
extension SPExploreViewController {
|
||||
|
||||
private func requestDataArr(page: Int) {
|
||||
|
16
ShortPlay/Class/Explore/View/SPExplorePlayerCell.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// SPExplorePlayerCell.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPExplorePlayerCell: SPPlayerListCell {
|
||||
|
||||
override var PlayerControlViewClass: SPPlayerControlView.Type {
|
||||
return SPExplorePlayerControlView.self
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
//
|
||||
// SPExplorePlayerControlView.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPExplorePlayerControlView: SPPlayerControlView {
|
||||
|
||||
|
||||
override var shortModel: SPShortModel? {
|
||||
didSet {
|
||||
desLabel.text = shortModel?.sp_description
|
||||
videoInfoView.shortModel = shortModel
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var desLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontRegular(ofSize: 12)
|
||||
label.textColor = .colorD2D2D2()
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var videoInfoView: SPVideoPlayerInfoView = {
|
||||
let view = SPVideoPlayerInfoView()
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.progressView.isHidden = true
|
||||
|
||||
_setupUI()
|
||||
}
|
||||
|
||||
@MainActor required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SPExplorePlayerControlView {
|
||||
|
||||
private func _setupUI() {
|
||||
addSubview(desLabel)
|
||||
addSubview(videoInfoView)
|
||||
|
||||
desLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(15)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-30)
|
||||
make.bottom.equalToSuperview().offset(-15)
|
||||
}
|
||||
|
||||
videoInfoView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(15)
|
||||
make.bottom.equalTo(desLabel.snp.top).offset(-10)
|
||||
}
|
||||
}
|
||||
}
|
112
ShortPlay/Class/Explore/View/SPVideoPlayerInfoView.swift
Normal file
@ -0,0 +1,112 @@
|
||||
//
|
||||
// SPVideoPlayerInfoView.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPVideoPlayerInfoView: UIView {
|
||||
|
||||
var shortModel: SPShortModel? {
|
||||
didSet {
|
||||
coverImageView.sp_setImage(url: shortModel?.image_url)
|
||||
titleLabel.text = shortModel?.name
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: UI属性
|
||||
private lazy var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.cornerRadius = 5
|
||||
view.layer.masksToBounds = true
|
||||
view.backgroundColor = .color000000(alpha: 0.27)
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var coverImageView: SPImageView = {
|
||||
let imageView = SPImageView()
|
||||
imageView.layer.cornerRadius = 5
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontRegular(ofSize: 13)
|
||||
label.textColor = .colorFFFFFF()
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var moreButton: JXButton = {
|
||||
let button = JXButton(type: .custom)
|
||||
button.colors = [UIColor.colorF56490().cgColor, UIColor.colorD568D2().cgColor]
|
||||
button.startPoint = .init(x: 0, y: 0.5)
|
||||
button.endPoint = .init(x: 1, y: 0.5)
|
||||
button.locations = [0, 1]
|
||||
button.setImage(UIImage(named: "play_icon_02"), for: .normal)
|
||||
button.setTitle("Series".localized, for: .normal)
|
||||
button.setTitleColor(.colorFFFFFF(), for: .normal)
|
||||
button.jx_font = .fontRegular(ofSize: 11)
|
||||
button.layer.cornerRadius = 10.5
|
||||
button.layer.masksToBounds = true
|
||||
button.space = 2
|
||||
button.addTarget(self, action: #selector(handleMoreButton), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func handleMoreButton() {
|
||||
let vc = SPPlayerDetailViewController()
|
||||
vc.shortPlayId = shortModel?.short_play_id
|
||||
self.viewController?.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension SPVideoPlayerInfoView {
|
||||
|
||||
private func _setupUI() {
|
||||
addSubview(bgView)
|
||||
addSubview(coverImageView)
|
||||
addSubview(titleLabel)
|
||||
addSubview(moreButton)
|
||||
|
||||
bgView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.width.equalTo(240)
|
||||
make.height.equalTo(54)
|
||||
}
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.bottom.top.equalToSuperview()
|
||||
make.width.equalTo(49)
|
||||
make.height.equalTo(66)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(coverImageView.snp.right).offset(10)
|
||||
make.top.equalTo(bgView).offset(5)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-18)
|
||||
}
|
||||
|
||||
moreButton.snp.makeConstraints { make in
|
||||
make.left.equalTo(coverImageView.snp.right).offset(10)
|
||||
make.bottom.equalToSuperview().offset(-5)
|
||||
make.width.equalTo(59)
|
||||
make.height.equalTo(21)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -9,15 +9,87 @@ import UIKit
|
||||
|
||||
class SPMineViewController: SPViewController {
|
||||
|
||||
private lazy var dataArr: [SPMineItem] = {
|
||||
let arr = [
|
||||
SPMineItem(type: .orderRecord, iconImage: UIImage(named: "order_record_icon_01"), title: "Order Record".localized),
|
||||
SPMineItem(type: .language, iconImage: UIImage(named: "language_icon_01"), title: "Language".localized),
|
||||
SPMineItem(type: .privacyPolicy, iconImage: UIImage(named: "privacy_policy_icon_01"), title: "Privacy Policy".localized),
|
||||
SPMineItem(type: .userAgreement, iconImage: UIImage(named: "user_agreement_icon_01"), title: "User Agreement".localized),
|
||||
SPMineItem(type: .helpCenter, iconImage: UIImage(named: "help_center_icon_01"), title: "Help Center".localized),
|
||||
SPMineItem(type: .aboutUs, iconImage: UIImage(named: "about_us_icon_01"), title: "About Us".localized),
|
||||
]
|
||||
return arr
|
||||
}()
|
||||
|
||||
private lazy var tableView: SPTableView = {
|
||||
let tableView = SPTableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.rowHeight = 50
|
||||
SPMineCell.registerCell(tableView: tableView)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
_setupUI()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.setNavigationNormalStyle()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SPMineViewController {
|
||||
|
||||
private func _setupUI() {
|
||||
view.addSubview(tableView)
|
||||
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: -------------- UITableViewDelegate & UITableViewDataSource --------------
|
||||
extension SPMineViewController: UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = SPMineCell.dequeueReusableCell(tableView: tableView, indexPath: indexPath)
|
||||
cell.item = dataArr[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return dataArr.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let item = dataArr[indexPath.row]
|
||||
switch item.type {
|
||||
case .privacyPolicy:
|
||||
let vc = SPWebViewController()
|
||||
vc.urlStr = SPPrivacyPolicyWebUrl
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
case .userAgreement:
|
||||
let vc = SPWebViewController()
|
||||
vc.urlStr = SPUserAgreementWebUrl
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SPMineViewController {
|
||||
|
||||
// func
|
||||
|
||||
}
|
||||
|
32
ShortPlay/Class/Mine/Model/SPMineItem.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// SPMineItem.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct SPMineItem {
|
||||
enum ItemType {
|
||||
///历史订单
|
||||
case orderRecord
|
||||
///语言
|
||||
case language
|
||||
///隐私协议
|
||||
case privacyPolicy
|
||||
///用户协议
|
||||
case userAgreement
|
||||
///帮助中心
|
||||
case helpCenter
|
||||
///关于我们
|
||||
case aboutUs
|
||||
}
|
||||
|
||||
|
||||
var type: ItemType?
|
||||
var iconImage: UIImage?
|
||||
var title: String?
|
||||
|
||||
|
||||
}
|
64
ShortPlay/Class/Mine/View/SPMineCell.swift
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// SPMineCell.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPMineCell: SPTableViewCell {
|
||||
|
||||
var item: SPMineItem? {
|
||||
didSet {
|
||||
iconImageView.image = item?.iconImage
|
||||
titleLabel.text = item?.title
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var iconImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontRegular(ofSize: 14)
|
||||
label.textColor = .colorFFFFFF(alpha: 0.9)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
self.contentView.backgroundColor = .colorFFFFFF(alpha: 0.04)
|
||||
|
||||
_setupUI()
|
||||
|
||||
}
|
||||
|
||||
@MainActor required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SPMineCell {
|
||||
|
||||
private func _setupUI() {
|
||||
contentView.addSubview(iconImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(10)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(40)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -23,8 +23,24 @@ class SPPlayerDetailViewController: SPPlayerListViewController {
|
||||
|
||||
private var detailModel: SPVideoDetailModel?
|
||||
|
||||
//MARK: UI属性
|
||||
///选集视图
|
||||
private weak var episodeView: SPEpisodeView?
|
||||
|
||||
private lazy var backButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(UIImage(named: "arrow_left_icon_01"), for: .normal)
|
||||
button.addTarget(self, action: #selector(handleBack), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontLight(ofSize: 15)
|
||||
label.textColor = .colorFFFFFF(alpha: 0.9)
|
||||
return label
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.autoNextEpisode = true
|
||||
@ -34,6 +50,8 @@ class SPPlayerDetailViewController: SPPlayerListViewController {
|
||||
requestDetailData()
|
||||
|
||||
_addAction()
|
||||
|
||||
_setupUI()
|
||||
}
|
||||
|
||||
|
||||
@ -56,11 +74,33 @@ class SPPlayerDetailViewController: SPPlayerListViewController {
|
||||
|
||||
extension SPPlayerDetailViewController {
|
||||
|
||||
private func _setupUI() {
|
||||
view.addSubview(backButton)
|
||||
view.addSubview(titleLabel)
|
||||
|
||||
backButton.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(5)
|
||||
make.top.equalToSuperview().offset(5 + kSPStatusbarHeight)
|
||||
make.width.height.equalTo(37)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(backButton.snp.right)
|
||||
make.centerY.equalTo(backButton)
|
||||
make.right.equalToSuperview().offset(-15)
|
||||
}
|
||||
}
|
||||
|
||||
private func _addAction() {
|
||||
self.viewModel.handleEpisode = { [weak self] in
|
||||
self?.onEpisode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SPPlayerDetailViewController {
|
||||
|
||||
|
||||
|
||||
private func onEpisode() {
|
||||
let view = SPEpisodeView()
|
||||
@ -93,6 +133,9 @@ extension SPPlayerDetailViewController: SPPlayerListViewControllerDataSource, SP
|
||||
|
||||
func sp_playerListViewController(_ viewController: SPPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) {
|
||||
self.episodeView?.currentIndex = indexPath.row
|
||||
let videoInfo = detailModel?.episodeList?[indexPath.row]
|
||||
|
||||
titleLabel.text = String(format: "kPlayerDetailTitleString".localized, "\(videoInfo?.episode ?? 0)", self.detailModel?.shortPlayInfo?.name ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,19 +7,20 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
struct SPSpeedModel {
|
||||
class SPSpeedModel: NSObject {
|
||||
|
||||
enum Speed: String {
|
||||
case x0_75 = "0.75x"
|
||||
case x0_5 = "0.5x"
|
||||
case x1 = "1.0x"
|
||||
case x1_25 = "1.25x"
|
||||
case x1_5 = "1.5x"
|
||||
case x1_75 = "1.75x"
|
||||
case x2 = "2.0x"
|
||||
|
||||
func getRate() -> Float {
|
||||
switch self {
|
||||
case .x0_75:
|
||||
return 0.75
|
||||
case .x0_5:
|
||||
return 0.5
|
||||
|
||||
case .x1:
|
||||
return 1
|
||||
@ -30,12 +31,53 @@ struct SPSpeedModel {
|
||||
case .x1_5:
|
||||
return 1.5
|
||||
|
||||
case .x1_75:
|
||||
return 1.75
|
||||
|
||||
case .x2:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func getAllSpeed() -> [SPSpeedModel] {
|
||||
return [
|
||||
SPSpeedModel(speed: .x0_5),
|
||||
SPSpeedModel(speed: .x1),
|
||||
SPSpeedModel(speed: .x1_25),
|
||||
SPSpeedModel(speed: .x1_5),
|
||||
SPSpeedModel(speed: .x1_75),
|
||||
SPSpeedModel(speed: .x2)
|
||||
]
|
||||
}
|
||||
|
||||
var speed: Speed = .x1
|
||||
|
||||
init(speed: Speed) {
|
||||
super.init()
|
||||
self.speed = speed
|
||||
}
|
||||
|
||||
func getRate() -> Float {
|
||||
return speed.getRate()
|
||||
}
|
||||
|
||||
func formatString() -> String {
|
||||
switch speed {
|
||||
case .x0_5:
|
||||
return "0.5x"
|
||||
case .x1:
|
||||
return "1.0x"
|
||||
case .x1_25:
|
||||
return "1.25x"
|
||||
case .x1_5:
|
||||
return "1.5x"
|
||||
case .x1_75:
|
||||
return "1.75x"
|
||||
case .x2:
|
||||
return "2.0x"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
149
ShortPlay/Class/Player/View/SPEpisodeMenuView.swift
Normal file
@ -0,0 +1,149 @@
|
||||
//
|
||||
// SPEpisodeMenuView.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPEpisodeMenuView: UIView {
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: kSPScreenWidth, height: 32)
|
||||
}
|
||||
|
||||
var didSelectedIndex: ((_ index: Int) -> Void)?
|
||||
|
||||
var dataArr: [String] = [] {
|
||||
didSet {
|
||||
self.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIndex: Int = 0 {
|
||||
didSet {
|
||||
self.buttonArr.forEach {
|
||||
$0.isSelected = $0.tag == selectedIndex
|
||||
}
|
||||
self.progressSlide()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private lazy var buttonArr: [UIButton] = []
|
||||
|
||||
//MARK: UI属性
|
||||
private lazy var progressView: SPGradientView = {
|
||||
let view = SPGradientView()
|
||||
view.colors = [UIColor.colorF56490().cgColor, UIColor.colorBF6BFF().cgColor]
|
||||
view.startPoint = .init(x: 0, y: 0.5)
|
||||
view.endPoint = .init(x: 1, y: 0.5)
|
||||
view.locations = [0, 1]
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var scrollView: SPScrollView = {
|
||||
let scrollView = SPScrollView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.showsHorizontalScrollIndicator = false
|
||||
return scrollView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func reloadData() {
|
||||
buttonArr.forEach {
|
||||
$0.removeFromSuperview()
|
||||
}
|
||||
buttonArr.removeAll()
|
||||
|
||||
let count = self.dataArr.count
|
||||
|
||||
var previousButton: UIButton?
|
||||
|
||||
dataArr.enumerated().forEach {
|
||||
let normalStrig = NSMutableAttributedString(string: $1)
|
||||
normalStrig.color = .color9D9D9D()
|
||||
normalStrig.font = .fontLight(ofSize: 14)
|
||||
|
||||
let selectedString = NSMutableAttributedString(string: $1)
|
||||
selectedString.color = .colorF564B6()
|
||||
selectedString.font = .fontMedium(ofSize: 14)
|
||||
|
||||
|
||||
let button = UIButton(type: .custom)
|
||||
button.tag = $0
|
||||
button.setAttributedTitle(normalStrig, for: .normal)
|
||||
button.setAttributedTitle(selectedString, for: .selected)
|
||||
button.setAttributedTitle(selectedString, for: [.selected, .highlighted])
|
||||
button.addTarget(self, action: #selector(handleButton), for: .touchUpInside)
|
||||
button.isSelected = $0 == selectedIndex
|
||||
|
||||
self.scrollView.addSubview(button)
|
||||
self.buttonArr.append(button)
|
||||
|
||||
if previousButton == nil {
|
||||
button.snp.makeConstraints { make in
|
||||
make.top.left.equalToSuperview()
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
} else if let previousButton = previousButton, count - 1 == $0 {
|
||||
button.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.left.equalTo(previousButton.snp.right).offset(25)
|
||||
make.height.equalTo(32)
|
||||
make.right.equalToSuperview()
|
||||
}
|
||||
} else if let previousButton = previousButton {
|
||||
button.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.left.equalTo(previousButton.snp.right).offset(25)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
}
|
||||
|
||||
previousButton = button
|
||||
}
|
||||
|
||||
progressSlide()
|
||||
}
|
||||
|
||||
private func progressSlide() {
|
||||
if self.selectedIndex >= self.buttonArr.count { return }
|
||||
let currentButton = self.buttonArr[self.selectedIndex]
|
||||
|
||||
self.progressView.snp.remakeConstraints { make in
|
||||
make.bottom.width.equalTo(currentButton)
|
||||
make.centerX.equalTo(currentButton)
|
||||
make.height.equalTo(2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func handleButton(sender: UIButton) {
|
||||
self.selectedIndex = sender.tag
|
||||
self.didSelectedIndex?(self.selectedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
extension SPEpisodeMenuView {
|
||||
|
||||
private func _setupUI() {
|
||||
addSubview(scrollView)
|
||||
scrollView.addSubview(progressView)
|
||||
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -18,17 +18,49 @@ class SPEpisodeView: HWPanModalContentView {
|
||||
var shortModel: SPShortModel? {
|
||||
didSet {
|
||||
coverImageView.sp_setImage(url: shortModel?.image_url)
|
||||
titleLabel.text = shortModel?.name
|
||||
desLabel.text = shortModel?.sp_description
|
||||
}
|
||||
}
|
||||
|
||||
var dataArr: [SPVideoInfoModel] = [] {
|
||||
didSet {
|
||||
self.collectionView.reloadData()
|
||||
|
||||
var menuDataArr = [String]()
|
||||
let totalEpisode = dataArr.count
|
||||
var index = 0
|
||||
var remainingEpisodes = totalEpisode
|
||||
|
||||
while remainingEpisodes > 0 {
|
||||
let minIndex = index * 30
|
||||
var maxIndex = minIndex + 29
|
||||
if maxIndex >= dataArr.count {
|
||||
maxIndex = dataArr.count - 1
|
||||
}
|
||||
|
||||
let minEpisode = dataArr[minIndex].episode ?? 0
|
||||
let maxEpisode = dataArr[maxIndex].episode ?? 0
|
||||
|
||||
if minEpisode == maxEpisode {
|
||||
menuDataArr.append("\(minEpisode)")
|
||||
} else {
|
||||
menuDataArr.append("\(minEpisode)-\(maxEpisode)")
|
||||
}
|
||||
|
||||
remainingEpisodes -= 30
|
||||
index += 1
|
||||
}
|
||||
|
||||
self.menuView.dataArr = menuDataArr
|
||||
}
|
||||
}
|
||||
|
||||
var didSelectedIndex: ((_ index: Int) -> Void)?
|
||||
|
||||
var isDecelerating = false
|
||||
var isDragging = false
|
||||
|
||||
//MARK: UI属性
|
||||
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
|
||||
let itemWidth = floor((kSPScreenWidth - 10 * 4 - 30) / 5)
|
||||
@ -64,6 +96,46 @@ class SPEpisodeView: HWPanModalContentView {
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontMedium(ofSize: 15)
|
||||
label.textColor = .colorFFFFFF()
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var desLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontLight(ofSize: 12)
|
||||
label.textColor = .color9D9D9D()
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var lineView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .color545454()
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var menuView: SPEpisodeMenuView = {
|
||||
let view = SPEpisodeMenuView()
|
||||
view.didSelectedIndex = { [weak self] index in
|
||||
guard let self = self else { return }
|
||||
var row = 0
|
||||
if index > 0 {
|
||||
row = index * 30 + 10
|
||||
let count = self.dataArr.count
|
||||
if row >= count {
|
||||
row = count - 1
|
||||
}
|
||||
}
|
||||
let indexPath = IndexPath.init(row: row, section: 0)
|
||||
self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true)
|
||||
|
||||
}
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_setupUI()
|
||||
@ -107,7 +179,11 @@ extension SPEpisodeView {
|
||||
private func _setupUI() {
|
||||
addSubview(indicatorView)
|
||||
addSubview(coverImageView)
|
||||
addSubview(self.collectionView)
|
||||
addSubview(titleLabel)
|
||||
addSubview(desLabel)
|
||||
addSubview(menuView)
|
||||
addSubview(lineView)
|
||||
addSubview(collectionView)
|
||||
|
||||
self.indicatorView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
@ -123,8 +199,34 @@ extension SPEpisodeView {
|
||||
make.height.equalTo(74)
|
||||
}
|
||||
|
||||
self.titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(self.coverImageView.snp.right).offset(12)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-15)
|
||||
make.centerY.equalTo(self.coverImageView)
|
||||
}
|
||||
|
||||
self.desLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(self.coverImageView)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-15)
|
||||
make.top.equalTo(self.coverImageView.snp.bottom).offset(8)
|
||||
}
|
||||
|
||||
self.menuView.snp.makeConstraints { make in
|
||||
make.left.right.equalTo(self.lineView)
|
||||
make.bottom.equalTo(self.lineView)
|
||||
}
|
||||
|
||||
self.lineView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(15)
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(self.desLabel.snp.bottom).offset(46)
|
||||
make.height.equalTo(0.7)
|
||||
}
|
||||
|
||||
self.collectionView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
// make.edges.equalToSuperview()
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalTo(self.lineView.snp.bottom).offset(15)
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,4 +252,50 @@ extension SPEpisodeView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if isDragging || isDecelerating {
|
||||
updateMuneSelectedIndex()
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
isDecelerating = false
|
||||
updateMuneSelectedIndex()
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
||||
isDecelerating = true
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
isDragging = true
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
isDragging = false
|
||||
}
|
||||
|
||||
|
||||
func updateMuneSelectedIndex() {
|
||||
let indexPathArr = collectionView.indexPathsForVisibleItems
|
||||
|
||||
var minRow = dataArr.count - 1
|
||||
var maxRow = 0
|
||||
|
||||
for indexPath in indexPathArr {
|
||||
if indexPath.row < minRow {
|
||||
minRow = indexPath.row
|
||||
}
|
||||
if indexPath.row > maxRow {
|
||||
maxRow = indexPath.row
|
||||
}
|
||||
}
|
||||
|
||||
let selectedIndex = maxRow / 30
|
||||
if menuView.selectedIndex != selectedIndex {
|
||||
menuView.selectedIndex = selectedIndex
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -39,6 +39,9 @@ class SPPlayerControlView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
var durationTime: Int = 0
|
||||
var currentTime: Int = 0
|
||||
|
||||
var isCurrent: Bool = false {
|
||||
didSet {
|
||||
updatePlayIconState()
|
||||
@ -46,6 +49,17 @@ class SPPlayerControlView: UIView {
|
||||
}
|
||||
|
||||
//MARK: UI属性
|
||||
///底部渐变层
|
||||
private lazy var bottomGradientView: SPGradientView = {
|
||||
let view = SPGradientView()
|
||||
view.colors = [UIColor.color000000(alpha: 0).cgColor, UIColor.color000000(alpha: 0.5).cgColor]
|
||||
view.startPoint = .init(x: 0.5, y: 0)
|
||||
view.endPoint = .init(x: 0.5, y: 1)
|
||||
view.locations = [0, 1]
|
||||
view.isUserInteractionEnabled = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private(set) lazy var progressView: SPPlayerProgressView = {
|
||||
let view = SPPlayerProgressView()
|
||||
view.panStart = { [weak self] in
|
||||
@ -97,6 +111,7 @@ class SPPlayerControlView: UIView {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: SPVideoAPI.updateShortCollectStateNotification, object: nil)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(hadlePlayAndOrPaused))
|
||||
tap.delegate = self
|
||||
self.addGestureRecognizer(tap)
|
||||
|
||||
|
||||
@ -136,10 +151,16 @@ class SPPlayerControlView: UIView {
|
||||
extension SPPlayerControlView {
|
||||
|
||||
private func sp_setupUI() {
|
||||
addSubview(bottomGradientView)
|
||||
addSubview(progressView)
|
||||
addSubview(playImageView)
|
||||
addSubview(rightFeatureView)
|
||||
|
||||
bottomGradientView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.height.equalTo(kSPTabbarSafeBottomMargin + 200)
|
||||
}
|
||||
|
||||
progressView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(10)
|
||||
make.centerX.equalToSuperview()
|
||||
@ -236,3 +257,15 @@ extension SPPlayerControlView {
|
||||
self.panProgressFinishBlock?(progress)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: -------------- UIGestureRecognizerDelegate --------------
|
||||
extension SPPlayerControlView: UIGestureRecognizerDelegate {
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
if touch.view != self {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,74 @@ import UIKit
|
||||
class SPPlayerDetailControlView: SPPlayerControlView {
|
||||
|
||||
|
||||
override var durationTime: Int {
|
||||
didSet {
|
||||
updateProgressTimeLabel()
|
||||
}
|
||||
}
|
||||
|
||||
override var currentTime: Int {
|
||||
didSet {
|
||||
updateProgressTimeLabel()
|
||||
}
|
||||
}
|
||||
|
||||
override var viewModel: SPPlayerListViewModel? {
|
||||
didSet {
|
||||
self.viewModel?.addObserver(self, forKeyPath: "speedModel", context: nil)
|
||||
updateSpeedButton()
|
||||
}
|
||||
}
|
||||
|
||||
override var isCurrent: Bool {
|
||||
didSet {
|
||||
if isCurrent {
|
||||
showSpeedSelectedView(isShow: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//MARK: UI属性
|
||||
private lazy var episodeButton: UIButton = {
|
||||
let button = createFeatureButton(title: "Episodes".localized, image: UIImage(named: "episodes_icon_01"))
|
||||
button.addTarget(self, action: #selector(handleEpisodeButton), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var progressTimeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontLight(ofSize: 12)
|
||||
label.textColor = .colorFFFFFF(alpha: 0.9)
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var speedButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setBackgroundImage(UIImage(color: .colorFFFFFF(alpha: 0.2)), for: .normal)
|
||||
button.setTitleColor(.colorFFFFFF(alpha: 0.9), for: .normal)
|
||||
button.titleLabel?.font = .fontLight(ofSize: 11)
|
||||
button.layer.cornerRadius = 10
|
||||
button.layer.masksToBounds = true
|
||||
button.addTarget(self, action: #selector(handleSpeedButton), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var speedSelectedView: SPSpeedSelectedView = {
|
||||
let view = SPSpeedSelectedView()
|
||||
view.isHidden = true
|
||||
view.didSelectedSpeed = { [weak self] speedModel in
|
||||
guard let self = self else { return }
|
||||
self.viewModel?.setSpeedPlay(speedModel: speedModel)
|
||||
self.showSpeedSelectedView(isShow: false)
|
||||
}
|
||||
return view
|
||||
}()
|
||||
|
||||
deinit {
|
||||
self.viewModel?.removeObserver(self, forKeyPath: "speedModel")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -26,6 +88,25 @@ class SPPlayerDetailControlView: SPPlayerControlView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
if keyPath == "speedModel" {
|
||||
updateSpeedButton()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func updateProgressTimeLabel() {
|
||||
let currentTime = self.currentTime.formatTimeGroup()
|
||||
let durationTime = self.durationTime.formatTimeGroup()
|
||||
|
||||
progressTimeLabel.text = "\(currentTime.1):\(currentTime.2)/\(durationTime.1):\(durationTime.2)"
|
||||
}
|
||||
|
||||
///更新速率按钮
|
||||
private func updateSpeedButton() {
|
||||
self.speedButton.setTitle(self.viewModel?.speedModel.formatString(), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
extension SPPlayerDetailControlView {
|
||||
@ -33,6 +114,34 @@ extension SPPlayerDetailControlView {
|
||||
private func _setupUI() {
|
||||
self.rightFeatureView.addArrangedSubview(episodeButton)
|
||||
|
||||
addSubview(progressTimeLabel)
|
||||
addSubview(speedButton)
|
||||
addSubview(speedSelectedView)
|
||||
|
||||
self.progressView.snp.remakeConstraints { make in
|
||||
make.left.equalToSuperview().offset(15)
|
||||
make.centerX.equalToSuperview()
|
||||
make.height.equalTo(30)
|
||||
make.bottom.equalToSuperview().offset(-(kSPTabbarSafeBottomMargin + 10))
|
||||
}
|
||||
|
||||
self.progressTimeLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(self.progressView)
|
||||
make.bottom.equalTo(self.progressView).offset(-12)
|
||||
}
|
||||
|
||||
speedButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(self.progressTimeLabel)
|
||||
make.right.equalToSuperview().offset(-15)
|
||||
make.width.equalTo(40)
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
|
||||
speedSelectedView.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
make.bottom.equalTo(self.speedButton.snp.top).offset(-30)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -41,4 +150,18 @@ extension SPPlayerDetailControlView {
|
||||
@objc private func handleEpisodeButton() {
|
||||
self.viewModel?.handleEpisode?()
|
||||
}
|
||||
|
||||
@objc private func handleSpeedButton() {
|
||||
showSpeedSelectedView(isShow: speedSelectedView.isHidden)
|
||||
}
|
||||
|
||||
///暂时或隐藏速率选择页面
|
||||
private func showSpeedSelectedView(isShow: Bool) {
|
||||
speedSelectedView.isHidden = !isShow
|
||||
rightFeatureView.isHidden = isShow
|
||||
|
||||
if isShow {
|
||||
self.speedSelectedView.currentSpeed = self.viewModel?.speedModel.speed ?? .x1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,9 @@ class SPPlayerListCell: SPCollectionViewCell, SPPlayerProtocol {
|
||||
didSet {
|
||||
self.controlView.progress = 0
|
||||
self.coverImageView.isHidden = false
|
||||
self.controlView.currentTime = 0
|
||||
self.controlView.durationTime = 0
|
||||
|
||||
self.controlView.videoInfo = videoInfo
|
||||
|
||||
player.setPlayUrl(url: videoInfo?.video_url ?? "")
|
||||
@ -169,6 +172,8 @@ extension SPPlayerListCell: SPPlayerDelegate {
|
||||
|
||||
func sp_playTimeChanged(_ player: SPPlayer, currentTime: Int, duration: Int) {
|
||||
controlView.progress = CGFloat(currentTime) / CGFloat(duration)
|
||||
controlView.currentTime = currentTime
|
||||
controlView.durationTime = duration
|
||||
}
|
||||
|
||||
func sp_firstRenderedStart(_ player: SPPlayer) {
|
||||
|
@ -32,10 +32,10 @@ class SPPlayerProgressView: UIView {
|
||||
///滑动进度
|
||||
private var panProgress: CGFloat = 0
|
||||
|
||||
var progressColor: UIColor = .red
|
||||
var currentProgress: UIColor = .white
|
||||
var progressColor: UIColor = .colorFFFFFF(alpha: 0.12)
|
||||
var currentProgress: UIColor = .colorFFFFFF(alpha: 0.48)
|
||||
|
||||
var lineWidth: CGFloat = 2
|
||||
var lineWidth: CGFloat = 5
|
||||
|
||||
///是否在滑动中
|
||||
private var isPaning: Bool = false
|
||||
|
56
ShortPlay/Class/Player/View/SPSpeedSelectedCell.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// SPSpeedSelectedCell.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPSpeedSelectedCell: SPCollectionViewCell {
|
||||
|
||||
var model: SPSpeedModel? {
|
||||
didSet {
|
||||
textLabel.text = model?.formatString()
|
||||
}
|
||||
}
|
||||
|
||||
var sp_isSelected: Bool = false {
|
||||
didSet {
|
||||
if sp_isSelected {
|
||||
self.contentView.layer.borderColor = UIColor.colorF564B6().cgColor
|
||||
self.contentView.backgroundColor = .colorF56490(alpha: 0.2)
|
||||
self.textLabel.textColor = .colorF564B6()
|
||||
} else {
|
||||
self.contentView.layer.borderColor = UIColor.clear.cgColor
|
||||
self.contentView.backgroundColor = .colorFFFFFF(alpha: 0.2)
|
||||
self.textLabel.textColor = .colorD2D2D2()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var textLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .fontLight(ofSize: 14)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.contentView.layer.cornerRadius = 7
|
||||
self.contentView.layer.masksToBounds = true
|
||||
self.contentView.layer.borderWidth = 1
|
||||
|
||||
contentView.addSubview(textLabel)
|
||||
|
||||
textLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
93
ShortPlay/Class/Player/View/SPSpeedSelectedView.swift
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// SPSpeedSelectedView.swift
|
||||
// ShortPlay
|
||||
//
|
||||
// Created by 曾觉新 on 2025/4/17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SPSpeedSelectedView: UIView {
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: kSPScreenWidth, height: 54)
|
||||
}
|
||||
|
||||
var didSelectedSpeed: ((_ model: SPSpeedModel) -> Void)?
|
||||
|
||||
var currentSpeed = SPSpeedModel.Speed.x1 {
|
||||
didSet {
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var dataArr: [SPSpeedModel] = SPSpeedModel.getAllSpeed()
|
||||
|
||||
|
||||
//MARK: UI属性
|
||||
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .horizontal
|
||||
layout.itemSize = .init(width: 70, height: 54)
|
||||
layout.minimumLineSpacing = 10
|
||||
layout.minimumInteritemSpacing = 10
|
||||
layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15)
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: SPCollectionView = {
|
||||
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.showsVerticalScrollIndicator = false
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
SPSpeedSelectedCell.registerCell(collectionView: collectionView)
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SPSpeedSelectedView {
|
||||
|
||||
private func _setupUI() {
|
||||
addSubview(collectionView)
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
|
||||
extension SPSpeedSelectedView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let model = dataArr[indexPath.row]
|
||||
let cell = SPSpeedSelectedCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
|
||||
cell.model = model
|
||||
cell.sp_isSelected = model.speed == currentSpeed
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return dataArr.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
|
||||
self.didSelectedSpeed?(dataArr[indexPath.row])
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -22,18 +22,19 @@ class SPPlayerListViewModel: NSObject {
|
||||
self?.handlePlayFinish?()
|
||||
}
|
||||
_currentPlayer?.isCurrent = true
|
||||
_currentPlayer?.rate = speedModel.getRate()
|
||||
}
|
||||
get {
|
||||
return _currentPlayer
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var speed: SPSpeedModel.Speed = .x1
|
||||
@objc dynamic private(set) lazy var speedModel = SPSpeedModel(speed: .x1)
|
||||
|
||||
///设置倍速播放
|
||||
func setSpeedPlay(speed: SPSpeedModel.Speed) {
|
||||
self.speed = speed
|
||||
currentPlayer?.rate = speed.getRate()
|
||||
func setSpeedPlay(speedModel: SPSpeedModel) {
|
||||
self.speedModel = speedModel
|
||||
currentPlayer?.rate = speedModel.getRate()
|
||||
}
|
||||
|
||||
|
||||
|
BIN
ShortPlay/Source/Assets.xcassets/icon/about_us_icon_01.imageset/About Us@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
ShortPlay/Source/Assets.xcassets/icon/about_us_icon_01.imageset/About Us@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.1 KiB |
22
ShortPlay/Source/Assets.xcassets/icon/about_us_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "About Us@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "About Us@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
ShortPlay/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "nav_back@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ShortPlay/Source/Assets.xcassets/icon/arrow_left_icon_01.imageset/nav_back@2x.png
vendored
Normal file
After Width: | Height: | Size: 116 B |
22
ShortPlay/Source/Assets.xcassets/icon/help_center_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Help Center@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Help Center@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ShortPlay/Source/Assets.xcassets/icon/help_center_icon_01.imageset/Help Center@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
ShortPlay/Source/Assets.xcassets/icon/help_center_icon_01.imageset/Help Center@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
22
ShortPlay/Source/Assets.xcassets/icon/language_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Language@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Language@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ShortPlay/Source/Assets.xcassets/icon/language_icon_01.imageset/Language@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
ShortPlay/Source/Assets.xcassets/icon/language_icon_01.imageset/Language@3x.png
vendored
Normal file
After Width: | Height: | Size: 5.3 KiB |
22
ShortPlay/Source/Assets.xcassets/icon/order_record_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Order Record@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Order Record@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ShortPlay/Source/Assets.xcassets/icon/order_record_icon_01.imageset/Order Record@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
ShortPlay/Source/Assets.xcassets/icon/order_record_icon_01.imageset/Order Record@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.3 KiB |
22
ShortPlay/Source/Assets.xcassets/icon/play_icon_02.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "play@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "play@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ShortPlay/Source/Assets.xcassets/icon/play_icon_02.imageset/play@2x.png
vendored
Normal file
After Width: | Height: | Size: 280 B |
BIN
ShortPlay/Source/Assets.xcassets/icon/play_icon_02.imageset/play@3x.png
vendored
Normal file
After Width: | Height: | Size: 431 B |
22
ShortPlay/Source/Assets.xcassets/icon/privacy_policy_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Privacy Policy@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Privacy Policy@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ShortPlay/Source/Assets.xcassets/icon/privacy_policy_icon_01.imageset/Privacy Policy@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
ShortPlay/Source/Assets.xcassets/icon/privacy_policy_icon_01.imageset/Privacy Policy@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.8 KiB |
22
ShortPlay/Source/Assets.xcassets/icon/user_agreement_icon_01.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "User Agreement@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "User Agreement@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ShortPlay/Source/Assets.xcassets/icon/user_agreement_icon_01.imageset/User Agreement@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
ShortPlay/Source/Assets.xcassets/icon/user_agreement_icon_01.imageset/User Agreement@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.3 KiB |
@ -20,3 +20,13 @@
|
||||
"Episodes" = "Episodes";
|
||||
"Save" = "Save";
|
||||
"Added" = "Added";
|
||||
"Series" = "Series";
|
||||
"Order Record" = "Order Record";
|
||||
"Language" = "Language";
|
||||
"Privacy Policy" = "Privacy Policy";
|
||||
"User Agreement" = "User Agreement";
|
||||
"Help Center" = "Help Center";
|
||||
"About Us" = "About Us";
|
||||
|
||||
///视频详情标题
|
||||
"kPlayerDetailTitleString" = "Episode %@ / %@";
|
||||
|
32
ShortPlay/Thirdparty/JXButton/JXButton.swift
vendored
@ -26,8 +26,36 @@ class JXButton: UIButton {
|
||||
private var titleRect: CGRect = .zero
|
||||
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
override class var layerClass: AnyClass {
|
||||
return CAGradientLayer.self
|
||||
}
|
||||
|
||||
var gradientLayer: CAGradientLayer {
|
||||
return self.layer as! CAGradientLayer
|
||||
}
|
||||
|
||||
var locations: [NSNumber]? {
|
||||
didSet {
|
||||
self.gradientLayer.locations = locations
|
||||
}
|
||||
}
|
||||
|
||||
var colors: [CGColor]? {
|
||||
didSet {
|
||||
self.gradientLayer.colors = colors
|
||||
}
|
||||
}
|
||||
|
||||
var startPoint: CGPoint = .zero {
|
||||
didSet {
|
||||
self.gradientLayer.startPoint = startPoint
|
||||
}
|
||||
}
|
||||
|
||||
var endPoint: CGPoint = .zero {
|
||||
didSet {
|
||||
self.gradientLayer.endPoint = endPoint
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
|