播放页面功能开发,我的页面开发

This commit is contained in:
曾觉新 2025-04-17 15:40:53 +08:00
parent 7a67b5824e
commit a2c6c0cc16
48 changed files with 1355 additions and 25 deletions

View File

@ -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"))

View 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)
}
}

View File

@ -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)
}
}

View 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")
}
}

View File

@ -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)

View File

@ -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) {

View 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
}
}

View File

@ -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)
}
}
}

View 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)
}
}
}

View File

@ -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
}

View 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?
}

View 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)
}
}
}

View File

@ -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 ?? "")
}
}

View File

@ -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"
}
}
}

View 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()
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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

View 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")
}
}

View 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])
}
}

View File

@ -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()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View 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
}
}

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -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 %@ / %@";

View File

@ -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 {