diff --git a/Fableon.xcodeproj/project.pbxproj b/Fableon.xcodeproj/project.pbxproj index 847eadf..4178b1a 100644 --- a/Fableon.xcodeproj/project.pbxproj +++ b/Fableon.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ B8B1DA3824F2148CEEF9F162 /* Pods_Fableon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4958FEE55B4555A94F11F00 /* Pods_Fableon.framework */; }; F301F6472E974B6300E76A90 /* FARecommendPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F301F6462E974B6300E76A90 /* FARecommendPlayerControlView.swift */; }; + F34296922EA0C60200A58F99 /* FAHomePlayHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34296912EA0C60200A58F99 /* FAHomePlayHistoryView.swift */; }; F37103312E978F8C00E7F171 /* FACollectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37103302E978F8C00E7F171 /* FACollectViewController.swift */; }; F37103352E97929F00E7F171 /* FACollectCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F37103342E97929F00E7F171 /* FACollectCell.xib */; }; F37103362E97929F00E7F171 /* FACollectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37103332E97929F00E7F171 /* FACollectCell.swift */; }; @@ -127,6 +128,7 @@ C4958FEE55B4555A94F11F00 /* Pods_Fableon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Fableon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC14083E24B746ED3DE2FE0C /* Pods-Fableon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fableon.release.xcconfig"; path = "Target Support Files/Pods-Fableon/Pods-Fableon.release.xcconfig"; sourceTree = ""; }; F301F6462E974B6300E76A90 /* FARecommendPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FARecommendPlayerControlView.swift; sourceTree = ""; }; + F34296912EA0C60200A58F99 /* FAHomePlayHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAHomePlayHistoryView.swift; sourceTree = ""; }; F37103302E978F8C00E7F171 /* FACollectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FACollectViewController.swift; sourceTree = ""; }; F37103332E97929F00E7F171 /* FACollectCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FACollectCell.swift; sourceTree = ""; }; F37103342E97929F00E7F171 /* FACollectCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FACollectCell.xib; sourceTree = ""; }; @@ -556,6 +558,7 @@ F371035D2E9E2E7400E7F171 /* FASearchRecommendCell.xib */, F37103642E9E3ABC00E7F171 /* FASearchResultCell.swift */, F37103652E9E3ABC00E7F171 /* FASearchResultCell.xib */, + F34296912EA0C60200A58F99 /* FAHomePlayHistoryView.swift */, ); path = V; sourceTree = ""; @@ -852,10 +855,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Fableon/Pods-Fableon-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Fableon/Pods-Fableon-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fableon/Pods-Fableon-frameworks.sh\"\n"; @@ -923,6 +930,7 @@ F3DCC0572E8A8EE800D58007 /* FAMeViewController.swift in Sources */, F3A792E82E77F8590097E0BC /* FAHomeModuleItem.swift in Sources */, F3DCC0452E89530200D58007 /* CGMutablePath+FARoundedCorner.swift in Sources */, + F34296922EA0C60200A58F99 /* FAHomePlayHistoryView.swift in Sources */, F37103882EA08B6F00E7F171 /* FANetworkMonitor.swift in Sources */, F37103672E9E3ABC00E7F171 /* FASearchResultCell.swift in Sources */, F371037B2EA0820C00E7F171 /* FASettingViewController.swift in Sources */, diff --git a/Fableon/Base/Request/FAAPI/FAAPI.swift b/Fableon/Base/Request/FAAPI/FAAPI.swift index 71fe0f2..d5b34cf 100644 --- a/Fableon/Base/Request/FAAPI/FAAPI.swift +++ b/Fableon/Base/Request/FAAPI/FAAPI.swift @@ -68,6 +68,19 @@ struct FAAPI { } } + ///上报播放时长 + static func requestUploadPlayTime(shortPlayId: String, videoId: String, seconds: Int) { + let parameters: [String : Any] = [ + "video_id" : videoId, + "short_play_id" : shortPlayId, + "play_seconds" : seconds + ] + + FANetworkManager.manager.request(FABaseURL + "/uploadHistorySeconds", method: .post, parameters: parameters, isToast: false) { (response: FANetworkManager.Response) in + + } + } + ///收藏 static func requestShortCollect(isCollect: Bool, shortPlayId: String, videoId: String?, isLoding: Bool = true, success: (() -> Void)?, failure: (() -> Void)? = nil) { let path: String diff --git a/Fableon/Class/Home/C/FAHomeViewController.swift b/Fableon/Class/Home/C/FAHomeViewController.swift index b93db5c..f64ec44 100644 --- a/Fableon/Class/Home/C/FAHomeViewController.swift +++ b/Fableon/Class/Home/C/FAHomeViewController.swift @@ -55,6 +55,12 @@ class FAHomeViewController: FAViewController { button.setImage(UIImage(named: "首页搜索i_ic"), for: .normal) return button }() + + private lazy var playHistoryView: FAHomePlayHistoryView = { + let view = FAHomePlayHistoryView() + view.isHidden = true + return view + }() deinit { NotificationCenter.default.removeObserver(self) @@ -65,9 +71,7 @@ class FAHomeViewController: FAViewController { NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: FANetworkMonitor.networkStatusDidChangeNotification, object: nil) fa_setupLayout() - self.viewModel.requestHomeData { [weak self] in - self?.collectionView.reloadData() - } + requestAllData(completer: nil) } override func viewWillAppear(_ animated: Bool) { @@ -75,18 +79,20 @@ class FAHomeViewController: FAViewController { self.navigationController?.setNavigationBarHidden(true, animated: true) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + requestPlayHistory() + } + override func handleHeaderRefresh(_ completer: (() -> Void)?) { - self.viewModel.requestHomeData { [weak self] in - self?.collectionView.reloadData() + requestAllData { [weak self] in self?.collectionView.fa_endHeaderRefreshing() } } @objc private func networkStatusDidChangeNotification() { if self.viewModel.dataArr.isEmpty, FANetworkMonitor.manager.isReachable == true { - self.viewModel.requestHomeData { [weak self] in - self?.collectionView.reloadData() - } + requestAllData(completer: nil) } } } @@ -97,6 +103,7 @@ extension FAHomeViewController { view.addSubview(titleView) view.addSubview(searchButton) view.addSubview(collectionView) + view.addSubview(playHistoryView) titleView.snp.makeConstraints { make in make.left.equalToSuperview().offset(16) @@ -112,6 +119,12 @@ extension FAHomeViewController { make.left.right.bottom.equalToSuperview() make.top.equalToSuperview().offset(UIScreen.navBarHeight) } + + playHistoryView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-10) + } } } @@ -245,3 +258,26 @@ extension FAHomeViewController: FAWaterfallMutiSectionDelegate { } } + + +extension FAHomeViewController { + + private func requestAllData(completer: (() -> Void)?) { + self.viewModel.requestHomeData { [weak self] in + self?.collectionView.reloadData() + completer?() + } + requestPlayHistory() + } + + private func requestPlayHistory() { + self.viewModel.requestPlayHistory { [weak self] in + guard let self = self else { return } + if let playHistory = self.viewModel.playHistory { + self.playHistoryView.model = playHistory + self.playHistoryView.isHidden = false + } + } + } + +} diff --git a/Fableon/Class/Home/V/FAHomePlayHistoryView.swift b/Fableon/Class/Home/V/FAHomePlayHistoryView.swift new file mode 100644 index 0000000..268f924 --- /dev/null +++ b/Fableon/Class/Home/V/FAHomePlayHistoryView.swift @@ -0,0 +1,128 @@ +// +// FAHomePlayHistoryView.swift +// Fableon +// +// Created by 长沙鸿瑶 on 2025/10/16. +// + +import UIKit + +class FAHomePlayHistoryView: UIView { + + var model: FAShortPlayModel? { + didSet { + coverImageView.fa_setImage(model?.image_url) + titleLabel.text = model?.name + epLabel.text = "Last Watch:Ep.##".localizedReplace(text: model?.current_episode ?? "") + } + } + + private lazy var coverImageView: FAImageView = { + let imageView = FAImageView() + imageView.layer.cornerRadius = 6 + return imageView + }() + + private lazy var bgView: UIView = { + let view = UIView() + view.layer.cornerRadius = 12 + view.layer.masksToBounds = true + view.layer.borderWidth = 1 + view.layer.borderColor = UIColor.BDCEFF.cgColor + view.fa_addEffectView(style: .light) + return view + }() + + private lazy var bgColorView: UIView = { + let view = UIView() + view.backgroundColor = ._5_CA_8_FF_0_2 + return view + }() + + private lazy var playView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "Group 2420")) + imageView.setContentHuggingPriority(.required, for: .horizontal) + imageView.setContentCompressionResistancePriority(.required, for: .horizontal) + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .medium) + label.textColor = .FFFFFF + return label + }() + + private lazy var epLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .FFFFFF + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + fa_setupLayout() + + let tap = UITapGestureRecognizer(target: self, action: #selector(handleBg)) + addGestureRecognizer(tap) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func handleBg() { + let vc = FAPlayerDetailViewController() + vc.shortPlayId = self.model?.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } + +} + +extension FAHomePlayHistoryView { + private func fa_setupLayout() { + addSubview(bgView) + bgView.addSubview(bgColorView) + addSubview(coverImageView) + bgView.addSubview(playView) + bgView.addSubview(titleLabel) + bgView.addSubview(epLabel) + + bgView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalToSuperview().offset(24) + make.height.equalTo(57) + } + + bgColorView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + coverImageView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.left.equalToSuperview().offset(25) + make.width.equalTo(56) + make.height.equalTo(74) + } + + playView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-10) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(96) + make.top.equalToSuperview().offset(12) + make.right.lessThanOrEqualTo(playView.snp.left).offset(-10) + } + + epLabel.snp.makeConstraints { make in + make.left.equalTo(titleLabel) + make.bottom.equalToSuperview().offset(-12) + } + + } +} diff --git a/Fableon/Class/Home/VM/FAHomeViewModel.swift b/Fableon/Class/Home/VM/FAHomeViewModel.swift index ae64c98..fab74f3 100644 --- a/Fableon/Class/Home/VM/FAHomeViewModel.swift +++ b/Fableon/Class/Home/VM/FAHomeViewModel.swift @@ -17,6 +17,9 @@ class FAHomeViewModel: ObservableObject { @Published var homeNewItem: FAHomeModuleItem? @Published var recommendedItem: FAHomeModuleItem? + ///播放历史 + var playHistory: FAShortPlayModel? + func requestHomeData(completer: (() -> Void)?) { FAAPI.requestHomeModulesData { [weak self] list in guard let self = self else { return } @@ -100,6 +103,16 @@ class FAHomeViewModel: ObservableObject { completer?() } } + + func requestPlayHistory(completer: (() -> Void)?) { + FAAPI.requestPlayHistorys(page: 1, pageSize: 1) { [weak self] listModel in + guard let self = self else { return } + if let model = listModel?.list?.first { + self.playHistory = model + } + completer?() + } + } } extension FAHomeViewModel { diff --git a/Fableon/Class/Player/V/FAPlayerDetailCell.swift b/Fableon/Class/Player/V/FAPlayerDetailCell.swift index 4fb60d5..ae930bd 100644 --- a/Fableon/Class/Player/V/FAPlayerDetailCell.swift +++ b/Fableon/Class/Player/V/FAPlayerDetailCell.swift @@ -40,3 +40,17 @@ class FAPlayerDetailCell: JXPlayerListCell { } + +extension FAPlayerDetailCell { + + override func jx_playerReadyToPlay(_ player: JXPlayer) { + super.jx_playerReadyToPlay(player) + guard let videoInfo = self.model as? FAVideoInfoModel else { return } + + let time = TimeInterval(videoInfo.play_seconds ?? 0) / 1000 + let durationTime = self.durationTime + if time > 1, durationTime > 0 { + self.seekTo(progress: Float(time) / Float(durationTime)) + } + } +} diff --git a/Fableon/Class/Player/VM/FAShortDetailViewModel.swift b/Fableon/Class/Player/VM/FAShortDetailViewModel.swift index 0273f5e..f297f69 100644 --- a/Fableon/Class/Player/VM/FAShortDetailViewModel.swift +++ b/Fableon/Class/Player/VM/FAShortDetailViewModel.swift @@ -14,8 +14,9 @@ class FAShortDetailViewModel: JXPlayerListViewModel, ObservableObject { private(set) var dataArr: [FAShortDetailModel] = [] var shortPlayId: String = "" - + ///上一次上报播放时长的节点 + private var lastUploadTime: TimeInterval = 0 var previousEpisode: FAVideoInfoModel? { guard dataArr.count > 0 else { return nil } @@ -43,14 +44,55 @@ class FAShortDetailViewModel: JXPlayerListViewModel, ObservableObject { if let model = model { self.dataArr.removeAll() self.dataArr.append(model) - self.playerListVC?.reloadData { - self.playerListVC?.play() + self.playerListVC?.reloadData { [weak self] in + guard let self = self else { return } + var targetIndexPath = IndexPath(row: 0, section: 0) + + if let videoInfo = model.video_info { + var row: Int? + model.episodeList?.enumerated().forEach { + if $1.short_play_video_id == videoInfo.short_play_video_id { + row = $0 + } + } + if let row = row { + targetIndexPath = .init(row: row, section: 0) + } + } + + self.playerListVC?.scrollToItem(indexPath: targetIndexPath, animated: false) } } completer?(code ?? -1) } } + + override func playProgressDidChange(player: any JXPlayerCell, time: TimeInterval) { + if time > 1 { + (player.model as? FAVideoInfoModel)?.play_seconds = Int(time) * 1000 + } + + if (time >= lastUploadTime + 5 || time < lastUploadTime) && time >= 5 { + lastUploadTime = time + uploadPlayTime() + } + } + + private func uploadPlayTime() { + let videoInfo = self.currentCell?.model as? FAVideoInfoModel + let currentTime = self.currentCell?.currentTime ?? 0 + let duration = self.currentCell?.durationTime ?? 0 + + var time = currentTime + if currentTime >= duration { + time = 0 + } + + guard let shortPlayId = videoInfo?.short_play_id, let videoId = videoInfo?.short_play_video_id else { return } + FAAPI.requestUploadPlayTime(shortPlayId: shortPlayId, videoId: videoId, seconds: Int(time) * 1000) + } + } extension FAShortDetailViewModel { diff --git a/Fableon/Source/Assets.xcassets/color/#BDCEFF.colorset/Contents.json b/Fableon/Source/Assets.xcassets/color/#BDCEFF.colorset/Contents.json new file mode 100644 index 0000000..b866ea5 --- /dev/null +++ b/Fableon/Source/Assets.xcassets/color/#BDCEFF.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xCE", + "red" : "0xBD" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Contents.json b/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Contents.json new file mode 100644 index 0000000..2dd0c49 --- /dev/null +++ b/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 2420@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 2420@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Group 2420@2x.png b/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Group 2420@2x.png new file mode 100644 index 0000000..e3dac5a Binary files /dev/null and b/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Group 2420@2x.png differ diff --git a/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Group 2420@3x.png b/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Group 2420@3x.png new file mode 100644 index 0000000..3da70fe Binary files /dev/null and b/Fableon/Source/Assets.xcassets/image/Group 2420.imageset/Group 2420@3x.png differ diff --git a/Fableon/Source/en.lproj/Localizable.strings b/Fableon/Source/en.lproj/Localizable.strings index af55d02..683432c 100644 --- a/Fableon/Source/en.lproj/Localizable.strings +++ b/Fableon/Source/en.lproj/Localizable.strings @@ -16,3 +16,4 @@ "Visit Website" = "Visit Website"; "Setting" = "Setting"; "Settings" = "Settings"; +"Last Watch:Ep.##" = "Last Watch:Ep.##";