增加上报阅读时长,修复BUG

This commit is contained in:
澜声世纪 2026-01-28 16:43:51 +08:00
parent 1b625064e6
commit 82ec2a370f
28 changed files with 385 additions and 221 deletions

View File

@ -90,6 +90,7 @@
85859E5C2EF3FC5F0020D282 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 85859E552EF3FC5F0020D282 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 85859E5C2EF3FC5F0020D282 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 85859E552EF3FC5F0020D282 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
85859E662EF3FC6E0020D282 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85859E632EF3FC6E0020D282 /* NotificationService.swift */; }; 85859E662EF3FC6E0020D282 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85859E632EF3FC6E0020D282 /* NotificationService.swift */; };
85859E682EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85859E672EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift */; }; 85859E682EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85859E672EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift */; };
85ACA3F92F28A7CD009D52B0 /* NRNovelReadViewModel+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ACA3F82F28A7C8009D52B0 /* NRNovelReadViewModel+View.swift */; };
85C1786B2F050AA400A8A76E /* Poppins-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C178682F050AA400A8A76E /* Poppins-Medium.ttf */; }; 85C1786B2F050AA400A8A76E /* Poppins-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C178682F050AA400A8A76E /* Poppins-Medium.ttf */; };
85C1786C2F050AA400A8A76E /* Poppins-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C1786A2F050AA400A8A76E /* Poppins-SemiBold.ttf */; }; 85C1786C2F050AA400A8A76E /* Poppins-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C1786A2F050AA400A8A76E /* Poppins-SemiBold.ttf */; };
85C1786D2F050AA400A8A76E /* Poppins-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C178672F050AA400A8A76E /* Poppins-Bold.ttf */; }; 85C1786D2F050AA400A8A76E /* Poppins-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 85C178672F050AA400A8A76E /* Poppins-Bold.ttf */; };
@ -600,6 +601,7 @@
85859E622EF3FC6E0020D282 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 85859E622EF3FC6E0020D282 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
85859E632EF3FC6E0020D282 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; }; 85859E632EF3FC6E0020D282 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
85859E672EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRHomeMustReadTodayTransformer.swift; sourceTree = "<group>"; }; 85859E672EFCD1D80020D282 /* NRHomeMustReadTodayTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRHomeMustReadTodayTransformer.swift; sourceTree = "<group>"; };
85ACA3F82F28A7C8009D52B0 /* NRNovelReadViewModel+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NRNovelReadViewModel+View.swift"; sourceTree = "<group>"; };
85C178672F050AA400A8A76E /* Poppins-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Bold.ttf"; sourceTree = "<group>"; }; 85C178672F050AA400A8A76E /* Poppins-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Bold.ttf"; sourceTree = "<group>"; };
85C178682F050AA400A8A76E /* Poppins-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Medium.ttf"; sourceTree = "<group>"; }; 85C178682F050AA400A8A76E /* Poppins-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Medium.ttf"; sourceTree = "<group>"; };
85C178692F050AA400A8A76E /* Poppins-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Regular.ttf"; sourceTree = "<group>"; }; 85C178692F050AA400A8A76E /* Poppins-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Regular.ttf"; sourceTree = "<group>"; };
@ -1088,6 +1090,7 @@
0373D9442ED57B7B0017DCC7 /* NRNovelDetailViewModel.swift */, 0373D9442ED57B7B0017DCC7 /* NRNovelDetailViewModel.swift */,
F34348EF2ED8381E00AA7E70 /* NRNovelReadViewModel.swift */, F34348EF2ED8381E00AA7E70 /* NRNovelReadViewModel.swift */,
F34991262EE282660039E939 /* NRNovelReadViewModel+Data.swift */, F34991262EE282660039E939 /* NRNovelReadViewModel+Data.swift */,
85ACA3F82F28A7C8009D52B0 /* NRNovelReadViewModel+View.swift */,
); );
path = VM; path = VM;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2615,6 +2618,7 @@
039810B62ED42D840006E317 /* NRHomeNovelNewArrivalsCell.swift in Sources */, 039810B62ED42D840006E317 /* NRHomeNovelNewArrivalsCell.swift in Sources */,
0373D95A2ED593D50017DCC7 /* NRSearchRecordCell.swift in Sources */, 0373D95A2ED593D50017DCC7 /* NRSearchRecordCell.swift in Sources */,
0373D9582ED5935D0017DCC7 /* NRSearchRecordView.swift in Sources */, 0373D9582ED5935D0017DCC7 /* NRSearchRecordView.swift in Sources */,
85ACA3F92F28A7CD009D52B0 /* NRNovelReadViewModel+View.swift in Sources */,
F343490C2ED9751800AA7E70 /* NRReadChapterCatalogModel.swift in Sources */, F343490C2ED9751800AA7E70 /* NRReadChapterCatalogModel.swift in Sources */,
F3B859862EE972F70095A9CC /* NRConsumptionRecordsCell.swift in Sources */, F3B859862EE972F70095A9CC /* NRConsumptionRecordsCell.swift in Sources */,
F34990C72EDFCE500039E939 /* NRNovelReadGradeView.swift in Sources */, F34990C72EDFCE500039E939 /* NRNovelReadGradeView.swift in Sources */,

View File

@ -162,6 +162,25 @@ struct NRStatAPI {
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<String>) in } NRNetwork.request(parameters: param) { (response: NRNetwork.Response<String>) in }
} }
///
static func nr_requestWatchNovelDuration(novelId: String, duration: Int, percent: Int) {
let parameters: [String : Any] = [
"request_id": "\(Int(Date().timeIntervalSince1970))",
"short_play_id" : novelId,
"duration" : duration,
"percent" : percent //
]
var param = NRNetwork.Parameters(path: "/watchShortDuration")
param.method = .post
param.isLoding = false
param.isToast = false
param.parameters = parameters
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<String>) in }
}
} }
extension NRStatAPI { extension NRStatAPI {

View File

@ -24,7 +24,7 @@ extension NRTargetType: TargetType {
var path: String { var path: String {
switch self { switch self {
case .request(let param): case .request(let param):
return "/readerhive" + param.path return NRBaseURLPrefix + param.path
} }
} }

View File

@ -7,7 +7,17 @@
import UIKit import UIKit
#if DEBUG
let NRBaseURL = "https://api-novel-test.guyantv.com"
let NRBaseURLPrefix = ""
//let NRBaseURL = "https://api-readerhive.readerhive.net"
//let NRBaseURLPrefix = "/readerhive"
#else
let NRBaseURL = "https://api-readerhive.readerhive.net" let NRBaseURL = "https://api-readerhive.readerhive.net"
let NRBaseURLPrefix = "/readerhive"
#endif
let NRWebBaseURL = "https://www.readerhive.net" let NRWebBaseURL = "https://www.readerhive.net"

View File

@ -153,9 +153,9 @@ extension UIViewController {
titleColor: UIColor = UINavigationBar.titleWhiteColor, titleColor: UIColor = UINavigationBar.titleWhiteColor,
isTranslucent: Bool = true isTranslucent: Bool = true
) { ) {
self.navigationController?.navigationBar.nr_setTranslucent(isTranslucent: isTranslucent) self.navigationController?.navigationBar.nr_setTranslucent(isTranslucent)
self.navigationController?.navigationBar.nr_setBackgroundColor(backgroundColor: backgroundColor) self.navigationController?.navigationBar.nr_setBackgroundColor(backgroundColor)
self.navigationController?.navigationBar.nr_setTitleTextAttributes(titleTextAttributes: [ self.navigationController?.navigationBar.nr_setTitleTextAttributes([
NSAttributedString.Key.font : titleFont, NSAttributedString.Key.font : titleFont,
NSAttributedString.Key.foregroundColor : titleColor NSAttributedString.Key.foregroundColor : titleColor
]) ])

View File

@ -22,10 +22,9 @@ class NRHomeNovelNextView: NRHomeNovelHeaderContentView {
let itemHeight = 150 / 100 * itemWidth + 68 let itemHeight = 150 / 100 * itemWidth + 68
let layout = UICollectionViewFlowLayout() let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal layout.itemSize = .init(width: floor(itemWidth), height: itemHeight)
layout.itemSize = .init(width: itemWidth, height: itemHeight) layout.minimumLineSpacing = 18
layout.minimumLineSpacing = 15 layout.minimumInteritemSpacing = 15
layout.minimumInteritemSpacing = 18
return layout return layout
}() }()
@ -36,10 +35,15 @@ class NRHomeNovelNextView: NRHomeNovelHeaderContentView {
collectionView.dataSource = self collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false collectionView.showsHorizontalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 16, bottom: 0, right: 16) collectionView.contentInset = .init(top: 0, left: 16, bottom: 0, right: 16)
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
collectionView.register(NRHomeNovelNextViewCell.self, forCellWithReuseIdentifier: "cell") collectionView.register(NRHomeNovelNextViewCell.self, forCellWithReuseIdentifier: "cell")
return collectionView return collectionView
}() }()
@MainActor deinit {
self.collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.titleLabel.text = "reader_home_title3".localized self.titleLabel.text = "reader_home_title3".localized
@ -51,6 +55,16 @@ class NRHomeNovelNextView: NRHomeNovelHeaderContentView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
let height = self.collectionView.contentSize.height + 1
collectionView.snp.updateConstraints { make in
make.height.equalTo(height)
}
}
}
} }
extension NRHomeNovelNextView { extension NRHomeNovelNextView {
@ -60,7 +74,7 @@ extension NRHomeNovelNextView {
collectionView.snp.makeConstraints { make in collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview() make.edges.equalToSuperview()
make.height.equalTo(collectionViewLayout.itemSize.height * 2 + collectionViewLayout.minimumInteritemSpacing) make.height.equalTo(1)
} }
} }

View File

@ -82,6 +82,8 @@ class NRStarGradeView: UIView {
private lazy var cosmosView: CosmosView = { private lazy var cosmosView: CosmosView = {
let view = CosmosView(settings: settings) let view = CosmosView(settings: settings)
view.setContentHuggingPriority(.required, for: .horizontal)
view.setContentCompressionResistancePriority(.required, for: .horizontal)
view.didTouchCosmos = { [weak self] rating in view.didTouchCosmos = { [weak self] rating in
guard let self = self else { return } guard let self = self else { return }
self.didTouch?(rating) self.didTouch?(rating)

View File

@ -21,7 +21,12 @@ class NRMeCoinsPackView: UIView {
return view return view
}() }()
private lazy var iconImageView = UIImageView(image: UIImage(named: "gift_icon_01")) private lazy var iconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "gift_icon_01"))
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
return imageView
}()
private lazy var titleLabel: UILabel = { private lazy var titleLabel: UILabel = {
let label = NRLabel() let label = NRLabel()
@ -41,7 +46,12 @@ class NRMeCoinsPackView: UIView {
return label return label
}() }()
private lazy var indicatorImageView = UIImageView(image: UIImage(named: "arrow_right_icon_07")) private lazy var indicatorImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "arrow_right_icon_07"))
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
return imageView
}()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)

View File

@ -109,7 +109,7 @@ class NRDetailRechargeView: NRPanModalContentView {
let label = UILabel() let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular) label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black label.textColor = .black
label.text = "Chapter Price".localized + ":" label.text = "reader_chapter_price".localized + ":"
return label return label
}() }()

View File

@ -115,7 +115,11 @@ extension NRNovelDetailHeaderView {
} }
private func updateNumLabel() { private func updateNumLabel() {
if num < 1000 {
numLabel.text = NSNumber(value: num).toString(minimumFractionDigits: minimumFractionDigits) numLabel.text = NSNumber(value: num).toString(minimumFractionDigits: minimumFractionDigits)
} else {
numLabel.text = NSNumber(value: num).formattedNumber()
}
} }
} }

View File

@ -32,7 +32,7 @@ class NRNovelReadBottomView: UIView {
}() }()
lazy var catalogButton: UIButton = { lazy var catalogButton: UIButton = {
let button = self.createButton(title: "Catalog".localized, icon: UIImage(named: "catalog_icon_01"), nightIcon: UIImage(named: "catalog_icon_02")) let button = self.createButton(title: "reader_book_catalog".localized, icon: UIImage(named: "catalog_icon_01"), nightIcon: UIImage(named: "catalog_icon_02"))
button.addAction(UIAction(handler: { [weak self] _ in button.addAction(UIAction(handler: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.viewModel?.showCatalogView() self.viewModel?.showCatalogView()

View File

@ -18,6 +18,13 @@ class NRNovelReadFinishHeaderView: UICollectionReusableView {
} }
} }
var didChangeContentHeight: ((_ height: CGFloat) -> Void)?
private lazy var contentView: NRScrollView = {
let contentView = NRScrollView()
contentView.addObserver(self, forKeyPath: "contentSize", context: nil)
return contentView
}()
private lazy var coverBgView: UIView = { private lazy var coverBgView: UIView = {
let view = NRGradientView() let view = NRGradientView()
@ -55,6 +62,8 @@ class NRNovelReadFinishHeaderView: UICollectionReusableView {
label.textColors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor] label.textColors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor]
label.textStartPoint = .init(x: 0, y: 0.5) label.textStartPoint = .init(x: 0, y: 0.5)
label.textEndPoint = .init(x: 1, y: 0.5) label.textEndPoint = .init(x: 1, y: 0.5)
label.numberOfLines = 0
label.textAlignment = .center
label.text = "reader_book_finished".localized label.text = "reader_book_finished".localized
return label return label
}() }()
@ -84,10 +93,14 @@ class NRNovelReadFinishHeaderView: UICollectionReusableView {
let label = UILabel() let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold) label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black label.textColor = .black
label.text = "read_finish_list_title".localized label.text = "reader_book_finished_list_title".localized
return label return label
}() }()
deinit {
self.contentView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
nr_setupUI() nr_setupUI()
@ -103,19 +116,31 @@ class NRNovelReadFinishHeaderView: UICollectionReusableView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
let height = self.contentView.contentSize.height + 1
self.didChangeContentHeight?(height)
}
}
} }
extension NRNovelReadFinishHeaderView { extension NRNovelReadFinishHeaderView {
private func nr_setupUI() { private func nr_setupUI() {
addSubview(coverBgView) addSubview(contentView)
contentView.addSubview(coverBgView)
coverBgView.addSubview(coverImageView) coverBgView.addSubview(coverImageView)
addSubview(coverDecorateView) contentView.addSubview(coverDecorateView)
addSubview(titleLabel) contentView.addSubview(titleLabel)
addSubview(textLabel) contentView.addSubview(textLabel)
addSubview(gradeView) contentView.addSubview(gradeView)
addSubview(lineView) contentView.addSubview(lineView)
addSubview(listTitleLabel) contentView.addSubview(listTitleLabel)
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
coverBgView.snp.makeConstraints { make in coverBgView.snp.makeConstraints { make in
make.centerX.equalToSuperview() make.centerX.equalToSuperview()
@ -138,12 +163,14 @@ extension NRNovelReadFinishHeaderView {
titleLabel.snp.makeConstraints { make in titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview() make.centerX.equalToSuperview()
make.top.equalTo(coverBgView.snp.bottom).offset(20) make.top.equalTo(coverBgView.snp.bottom).offset(20)
// make.right.lessThanOrEqualToSuperview().offset(-16)
make.right.lessThanOrEqualTo(gradeView)
} }
textLabel.snp.makeConstraints { make in textLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview() make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(16)
make.top.equalTo(titleLabel.snp.bottom).offset(6) make.top.equalTo(titleLabel.snp.bottom).offset(6)
make.right.lessThanOrEqualTo(gradeView)
} }
gradeView.snp.makeConstraints { make in gradeView.snp.makeConstraints { make in
@ -162,6 +189,7 @@ extension NRNovelReadFinishHeaderView {
listTitleLabel.snp.makeConstraints { make in listTitleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16) make.left.equalToSuperview().offset(16)
make.top.equalTo(lineView.snp.bottom).offset(16) make.top.equalTo(lineView.snp.bottom).offset(16)
make.bottom.equalToSuperview().offset(-16)
} }
} }

View File

@ -38,6 +38,7 @@ class NRNovelReadStarGradeView: UIView {
let label = UILabel() let label = UILabel()
label.font = .font(ofSize: 12, weight: .medium) label.font = .font(ofSize: 12, weight: .medium)
label.textColor = .black label.textColor = .black
label.numberOfLines = 0
return label return label
}() }()
@ -46,8 +47,11 @@ class NRNovelReadStarGradeView: UIView {
view.filledImage = UIImage(named: "star_icon_04") view.filledImage = UIImage(named: "star_icon_04")
view.emptyImage = UIImage(named: "star_icon_05") view.emptyImage = UIImage(named: "star_icon_05")
view.updateOnTouch = true view.updateOnTouch = true
view.fillMode = .full // view.fillMode = .full
view.fillMode = .precise
view.didFinishTouching = self.didFinishTouchingGrade view.didFinishTouching = self.didFinishTouchingGrade
view.setContentHuggingPriority(.required, for: .horizontal)
view.setContentCompressionResistancePriority(.required, for: .horizontal)
return view return view
}() }()
@ -102,6 +106,7 @@ extension NRNovelReadStarGradeView {
label.snp.makeConstraints { make in label.snp.makeConstraints { make in
make.centerY.equalToSuperview() make.centerY.equalToSuperview()
make.right.lessThanOrEqualTo(gradeView.snp.left).offset(-10)
make.left.equalToSuperview().offset(12) make.left.equalToSuperview().offset(12)
} }

View File

@ -119,6 +119,10 @@ extension NRNovelReadFinishViewController: UICollectionViewDelegate, UICollectio
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! NRNovelReadFinishHeaderView let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! NRNovelReadFinishHeaderView
view.didChangeContentHeight = { [weak self] height in
guard let self = self else { return }
self.collectionViewLayout.headerReferenceSize = .init(width: UIScreen.width, height: height)
}
view.novelModel = self.viewModel?.novelModel view.novelModel = self.viewModel?.novelModel
return view return view
} }

View File

@ -50,6 +50,7 @@ extension NRNovelReaderViewController {
if let vc = viewController as? NRNovelReadBaseViewController { if let vc = viewController as? NRNovelReadBaseViewController {
let model = vc.catalogModel let model = vc.catalogModel
let pageModel = vc.pageModel
let lockCoins = model?.coins ?? 0 let lockCoins = model?.coins ?? 0
let myCoins = NRLoginManager.manager.userInfo?.totalCoins ?? 0 let myCoins = NRLoginManager.manager.userInfo?.totalCoins ?? 0
if model?.is_lock == true { if model?.is_lock == true {
@ -59,6 +60,13 @@ extension NRNovelReaderViewController {
self.viewModel.openRechargeView() self.viewModel.openRechargeView()
} }
} }
if pageModel?.pageType != .textPart {
self.viewModel.statWatchNovelDuration()
} else {
self.viewModel.startWatchStartDate()
}
} }
} }

View File

@ -114,6 +114,8 @@ class NRNovelReaderViewController: NRViewController {
UIApplication.shared.isIdleTimerDisabled = true UIApplication.shared.isIdleTimerDisabled = true
UIScreen.main.brightness = NRNovelReadSetManager.manager.brightness UIScreen.main.brightness = NRNovelReadSetManager.manager.brightness
self.viewModel.startWatchStartDate()
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
@ -124,6 +126,7 @@ class NRNovelReaderViewController: NRViewController {
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
UIScreen.main.brightness = oldBrightness UIScreen.main.brightness = oldBrightness
self.viewModel.statWatchNovelDuration()
} }
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
@ -217,6 +220,8 @@ extension NRNovelReaderViewController {
/// ///
func loadData() { func loadData() {
Task { Task {
self.viewModel.statWatchNovelDuration()
NRHud.show() NRHud.show()
// //
await self.viewModel.requestNovelDetail() await self.viewModel.requestNovelDetail()
@ -324,11 +329,13 @@ extension NRNovelReaderViewController {
@objc private func willResignActiveNotification() { @objc private func willResignActiveNotification() {
guard self.isViewDidAppear else { return } guard self.isViewDidAppear else { return }
UIScreen.main.brightness = oldBrightness UIScreen.main.brightness = oldBrightness
self.viewModel.statWatchNovelDuration()
} }
@objc private func didBecomeActiveNotification() { @objc private func didBecomeActiveNotification() {
guard self.isViewDidAppear else { return } guard self.isViewDidAppear else { return }
UIScreen.main.brightness = NRNovelReadSetManager.manager.brightness UIScreen.main.brightness = NRNovelReadSetManager.manager.brightness
self.viewModel.startWatchStartDate()
} }
} }

View File

@ -16,7 +16,7 @@ class NRNovelDetailViewModel: NSObject {
var recommandDataArr: [NRNovelModel]? var recommandDataArr: [NRNovelModel]?
func requestDetailData() async { func requestDetailData() async {
let (model, id, msg) = await NRNovelAPI.requestDetail(novelId) let (model, _, _) = await NRNovelAPI.requestDetail(novelId)
await MainActor.run { await MainActor.run {
if let model = model { if let model = model {

View File

@ -286,6 +286,33 @@ extension NRNovelReadViewModel {
} }
} }
func startWatchStartDate() {
let (model, pageModel) = self.getCurrentPageData()
guard let model = model else { return }
guard let pageModel = pageModel else { return }
if model.is_lock == false, self.watchStartDate == nil, pageModel.pageType == .textPart {
if self.vc?.isViewDidAppear == true {
self.watchStartDate = Date()
}
}
}
///
func statWatchNovelDuration() {
guard let watchStartDate = self.watchStartDate else { return }
self.watchStartDate = nil
let (catalogModel, _) = getCurrentPageData()
// guard let chapterModel = catalogModel?.chapterModel else { return }
let totalEp = CGFloat(self.novelModel?.episode_total ?? 1)
// let currentEp = CGFloat(NSNumber(pointer: chapterModel.episode ?? "0").floatValue)
let currentEp = CGFloat(self.currentPageIndexPath.section)
let nowDate = Date()
let duration = Int(nowDate.timeIntervalSince1970 - watchStartDate.timeIntervalSince1970)
let percent = currentEp / totalEp * 100
NRStatAPI.nr_requestWatchNovelDuration(novelId: self.novelId, duration: duration, percent: Int(percent))
}
} }

View File

@ -0,0 +1,193 @@
//
// NRNovelReadViewModel+View.swift
// ReaderHive
//
// Created by on 2026/1/27.
//
import UIKit
import HWPanModal
import YYCategories
extension NRNovelReadViewModel {
///退
func backReadPage() {
Task {
guard let isShowModel = await NRNovelAPI.requestShowRecommendPop(id: self.novelId), isShowModel.is_pop_up == true else {
_backReadPage()
return
}
await MainActor.run {
let alert = NRAlert(title: "reader_recommend_book".localized, detail: "reader_recommend_book_tip".localized, topIconImage: UIImage(named: "alert_top_icon_01"), highlightButtonText: "reader_recommend_book_ok".localized)
alert.closeHandle = { [weak self] in
guard let self = self else { return }
self._backReadPage()
}
alert.highlightHandle = { [weak self] in
guard let self = self else { return }
NRNovelAPI.requestConfirmRecommend(self.novelId)
self._backReadPage()
}
alert.show()
}
}
}
@MainActor
private func _backReadPage() {
self.vc?.navigationController?.popViewController(animated: true)
}
func showAllMenuView(isShow: Bool) {
self.showTopView(isShow: isShow)
self.showBottomView(isShow: isShow)
}
///
func showTopView(isShow: Bool) {
guard showTop != isShow else { return }
self.showTop = isShow
self.vc?.setNeedsStatusBarAppearanceUpdate()
UIView.animate(withDuration: NRNovelReadSetManager.manager.animateDuration) { [weak self] in
guard let self = self else { return }
if isShow {
self.topView?.transform = CGAffineTransform(translationX: 0, y: NRNovelReadSetManager.manager.topViewHeight)
} else {
self.topView?.transform = CGAffineTransform.identity
}
} completion: { _ in
}
}
///
func showBottomView(isShow: Bool) {
UIView.animate(withDuration: NRNovelReadSetManager.manager.animateDuration) { [weak self] in
guard let self = self else { return }
if isShow {
self.bottomView?.transform = CGAffineTransform(translationX: 0, y: -NRNovelReadSetManager.manager.bottomViewHeight)
} else {
self.bottomView?.transform = CGAffineTransform.identity
}
} completion: { _ in
}
}
///
func showMoreView() {
guard let model = self.novelModel else { return }
let view = NRNovelReadGradeView()
view.model = model
view.present(in: nil)
}
///
func showSettingView() {
let view = NRNovelReadSettingView()
view.present(in: nil)
}
///
func showCatalogView() {
let (catalogModel, _) = self.getCurrentPageData()
let view = NRNovelReaderCatalogView()
view.novelModel = self.novelModel
view.currentCatalogModel = catalogModel
view.catalogDataArr = self.chapterCatalogList
view.didSelected = { [weak self] index in
guard let self = self else { return }
if index != self.currentPageIndexPath.section {
self.skip(chapterIndex: index)
}
}
view.show()
}
///
func openRechargeView() {
guard self.popView == nil else { return }
let (catalogModel, _) = self.getCurrentPageData()
guard let catalogModel = catalogModel else { return }
self.statWatchNovelDuration()
self.payDataRequest = NRPayDataRequest()
if let model = NRIapManager.manager.payDateModel {
_openRechargeView(model, catalogModel)
self.payDataRequest?.requestProducts(isLoding: false) { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
if let view = self.popView as? NRDetailRechargeView {
view.payModel = model
}
}
} else {
self.payDataRequest?.requestProducts(isLoding: true) { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
self._openRechargeView(model, catalogModel)
}
}
}
private func _openRechargeView(_ payModel: NRPayDateModel, _ catalogModel: NRReadChapterCatalogModel) {
guard self.popView == nil else { return }
NRStatAPI.nr_requestEventStat(orderCode: nil, shortPlayId: self.novelId, videoId: catalogModel.id, eventKey: .payTemplateDialog, errorMsg: nil, otherParamenters: [
"event_name" : "pay open"
])
let view = NRDetailRechargeView()
view.price = catalogModel.coins
view.payModel = payModel
view.worksId = self.novelId
view.chapterId = catalogModel.id
view.buyFinishHandle = { [weak self] in
guard let self = self else { return }
self.targetCatalogModel = catalogModel
self.vc?.loadData()
}
// view.didDismissHandle = { [weak self] in
// guard let self = self else { return }
// self.showVipRetainAlert(catalogModel)
// }
view.present(in: nil)
self.popView = view
}
///
private func showVipRetainAlert(_ catalogModel: NRReadChapterCatalogModel) {
payDataRequest = NRPayDataRequest()
payDataRequest?.requestVipRetainPayInfo { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
let view = NRVipRetainAlert()
view.model = model
view.worksId = self.novelId
view.chapterId = catalogModel.id
view.buyFinishHandle = { [weak self] in
guard let self = self else { return }
self.targetCatalogModel = catalogModel
self.vc?.loadData()
}
view.show(in: nil)
}
}
}

View File

@ -27,7 +27,7 @@ class NRNovelReadViewModel: NSObject {
} }
} }
private(set) var showTop = false var showTop = false
/// ///
lazy var chapterCatalogList: [NRReadChapterCatalogModel] = [] lazy var chapterCatalogList: [NRReadChapterCatalogModel] = []
@ -49,190 +49,13 @@ class NRNovelReadViewModel: NSObject {
weak var popView: UIView? weak var popView: UIView?
private var payDataRequest = NRPayDataRequest() var payDataRequest: NRPayDataRequest?
///
var watchStartDate: Date?
} }
extension NRNovelReadViewModel {
///退
func backReadPage() {
Task {
guard let isShowModel = await NRNovelAPI.requestShowRecommendPop(id: self.novelId), isShowModel.is_pop_up == true else {
_backReadPage()
return
}
await MainActor.run {
let alert = NRAlert(title: "reader_recommend_book".localized, detail: "reader_recommend_book_tip".localized, topIconImage: UIImage(named: "alert_top_icon_01"), highlightButtonText: "reader_recommend_book_ok".localized)
alert.closeHandle = { [weak self] in
guard let self = self else { return }
self._backReadPage()
}
alert.highlightHandle = { [weak self] in
guard let self = self else { return }
NRNovelAPI.requestConfirmRecommend(self.novelId)
self._backReadPage()
}
alert.show()
}
}
}
@MainActor
private func _backReadPage() {
self.vc?.navigationController?.popViewController(animated: true)
}
func showAllMenuView(isShow: Bool) {
self.showTopView(isShow: isShow)
self.showBottomView(isShow: isShow)
}
///
func showTopView(isShow: Bool) {
guard showTop != isShow else { return }
self.showTop = isShow
self.vc?.setNeedsStatusBarAppearanceUpdate()
UIView.animate(withDuration: NRNovelReadSetManager.manager.animateDuration) { [weak self] in
guard let self = self else { return }
if isShow {
self.topView?.transform = CGAffineTransform(translationX: 0, y: NRNovelReadSetManager.manager.topViewHeight)
} else {
self.topView?.transform = CGAffineTransform.identity
}
} completion: { _ in
}
}
///
func showBottomView(isShow: Bool) {
UIView.animate(withDuration: NRNovelReadSetManager.manager.animateDuration) { [weak self] in
guard let self = self else { return }
if isShow {
self.bottomView?.transform = CGAffineTransform(translationX: 0, y: -NRNovelReadSetManager.manager.bottomViewHeight)
} else {
self.bottomView?.transform = CGAffineTransform.identity
}
} completion: { _ in
}
}
///
func showMoreView() {
guard let model = self.novelModel else { return }
let view = NRNovelReadGradeView()
view.model = model
view.present(in: nil)
}
///
func showSettingView() {
let view = NRNovelReadSettingView()
view.present(in: nil)
}
///
func showCatalogView() {
let (catalogModel, _) = self.getCurrentPageData()
let view = NRNovelReaderCatalogView()
view.novelModel = self.novelModel
view.currentCatalogModel = catalogModel
view.catalogDataArr = self.chapterCatalogList
view.didSelected = { [weak self] index in
guard let self = self else { return }
if index != self.currentPageIndexPath.section {
self.skip(chapterIndex: index)
}
}
view.show()
}
///
func openRechargeView() {
guard self.popView == nil else { return }
let (catalogModel, _) = self.getCurrentPageData()
guard let catalogModel = catalogModel else { return }
if let model = NRIapManager.manager.payDateModel {
_openRechargeView(model, catalogModel)
self.payDataRequest.requestProducts(isLoding: false) { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
if let view = self.popView as? NRDetailRechargeView {
view.payModel = model
}
}
} else {
self.payDataRequest.requestProducts(isLoding: true) { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
self._openRechargeView(model, catalogModel)
}
}
}
private func _openRechargeView(_ payModel: NRPayDateModel, _ catalogModel: NRReadChapterCatalogModel) {
guard self.popView == nil else { return }
NRStatAPI.nr_requestEventStat(orderCode: nil, shortPlayId: self.novelId, videoId: catalogModel.id, eventKey: .payTemplateDialog, errorMsg: nil, otherParamenters: [
"event_name" : "pay open"
])
let view = NRDetailRechargeView()
view.price = catalogModel.coins
view.payModel = payModel
view.worksId = self.novelId
view.chapterId = catalogModel.id
view.buyFinishHandle = { [weak self] in
guard let self = self else { return }
self.targetCatalogModel = catalogModel
self.vc?.loadData()
}
// view.didDismissHandle = { [weak self] in
// guard let self = self else { return }
// self.showVipRetainAlert(catalogModel)
// }
view.present(in: nil)
self.popView = view
}
///
private func showVipRetainAlert(_ catalogModel: NRReadChapterCatalogModel) {
payDataRequest.requestVipRetainPayInfo { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
let view = NRVipRetainAlert()
view.model = model
view.worksId = self.novelId
view.chapterId = catalogModel.id
view.buyFinishHandle = { [weak self] in
guard let self = self else { return }
self.targetCatalogModel = catalogModel
self.vc?.loadData()
}
view.show(in: nil)
}
}
}
//MARK: UIGestureRecognizerDelegate //MARK: UIGestureRecognizerDelegate
extension NRNovelReadViewModel: UIGestureRecognizerDelegate { extension NRNovelReadViewModel: UIGestureRecognizerDelegate {

View File

@ -85,7 +85,7 @@ class NRCoinsPackConfirmView: NRPanModalContentView {
var configuration = UIButton.Configuration.plain() var configuration = UIButton.Configuration.plain()
configuration.background.image = UIImage(named: "gradient_color_01") configuration.background.image = UIImage(named: "gradient_color_01")
configuration.background.cornerRadius = 24 configuration.background.cornerRadius = 24
configuration.attributedTitle = AttributedString("Continue".localized, attributes: AttributeContainer([ configuration.attributedTitle = AttributedString("reader_continue".localized, attributes: AttributeContainer([
.font : UIFont.font(ofSize: 14, weight: .bold), .font : UIFont.font(ofSize: 14, weight: .bold),
.foregroundColor : UIColor.white .foregroundColor : UIColor.white
])) ]))

View File

@ -15,7 +15,8 @@ class NRStoreVipCell: UICollectionViewCell {
var model: NRPayItem? { var model: NRPayItem? {
didSet { didSet {
titleLabel.text = model?.brief titleLabel.text = model?.brief
desLabel.text = model?.nr_description // desLabel.text = model?.nr_description
desLabel.text = "reader_my_vip_tips".localized
if let coins = model?.send_coins, coins > 0 { if let coins = model?.send_coins, coins > 0 {
extraLabel.text = "+\(coins) \("reader_extra".localized)" extraLabel.text = "+\(coins) \("reader_extra".localized)"
@ -35,10 +36,6 @@ class NRStoreVipCell: UICollectionViewCell {
} }
if let price = offerPrice { if let price = offerPrice {
// let priceString = NSMutableAttributedString(string: currency + price)
// priceString.yy_font = .font(ofSize: 28, weight: .init(800))
// priceLabel.attributedText = priceString
//
let oldPriceStr = NSMutableAttributedString(string: oldPrice) let oldPriceStr = NSMutableAttributedString(string: oldPrice)
oldPriceStr.yy_strikethroughColor = oldPriceLabel.textColor oldPriceStr.yy_strikethroughColor = oldPriceLabel.textColor
oldPriceStr.yy_strikethroughStyle = .double oldPriceStr.yy_strikethroughStyle = .double

View File

@ -18,6 +18,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
nr_registThirdparty(application, didFinishLaunchingWithOptions: launchOptions) nr_registThirdparty(application, didFinishLaunchingWithOptions: launchOptions)
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: NRNetworkReachableManager.networkStatusDidChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: NRNetworkReachableManager.networkStatusDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(localizedDidChangeNotification), name: NRLocalizedManager.localizedDidChangeNotification, object: nil)
setConfig() setConfig()
@ -51,6 +52,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.refreshAppData() self.refreshAppData()
} }
@objc private func localizedDidChangeNotification() {
self.refreshAppData()
}
} }
private extension AppDelegate { private extension AppDelegate {
@ -63,5 +67,7 @@ private extension AppDelegate {
NRIapManager.manager.preloadingProducts() NRIapManager.manager.preloadingProducts()
} }
} }

View File

@ -21,7 +21,7 @@ class NRCoinsPackAlert: NRAlert {
titleLabel.numberOfLines = 0 titleLabel.numberOfLines = 0
titleLabel.font = .font(ofSize: 20, weight: .medium) titleLabel.font = .font(ofSize: 20, weight: .medium)
titleLabel.textColor = .F_9710_D titleLabel.textColor = .F_9710_D
titleLabel.text = "coins_pack_alert_title".localized titleLabel.text = "reader_my_daily_reward".localized
let coinsView = UIView() let coinsView = UIView()

View File

@ -33,6 +33,7 @@ struct NREmpty {
view?.actionBtnBackGroundColor = .clear view?.actionBtnBackGroundColor = .clear
view?.actionBtnMargin = 16 view?.actionBtnMargin = 16
view?.actionBtnHorizontalMargin = 25 view?.actionBtnHorizontalMargin = 25
view?.actionBtnWidth = 0
return view! return view!

View File

@ -26,7 +26,7 @@ class NRUserInfo: NSObject, SmartCodable, NSSecureCoding {
if let name = family_name, !name.isEmpty { if let name = family_name, !name.isEmpty {
return name return name
} else { } else {
return "Visitor" return "reader_visitor".localized
} }
} }

View File

@ -62,11 +62,11 @@ class NRNovelReadSetManager: NSObject {
var paragraphSpacing: CGFloat { var paragraphSpacing: CGFloat {
switch self.readSet.paragraphSpacingType { switch self.readSet.paragraphSpacingType {
case .small: case .small:
return 15 return 6
case .standard: case .standard:
return 17 return 8
case .large: case .large:
return 19 return 10
} }
} }

View File

@ -149,13 +149,15 @@
"reader_open_notification_later" = "Later"; "reader_open_notification_later" = "Later";
"reader_default" = "Default"; "reader_default" = "Default";
"reader_login" = "Log in"; "reader_login" = "Log in";
"Chapter Price" = "Chapter Price"; "reader_chapter_price" = "Chapter Price";
"reader_my_vip_tips" = "Unrestricted access to all series!";
"reader_visitor" = "Visitor";
"vip_retain_alert_text" = "Unlock every show you love!"; "vip_retain_alert_text" = "Unlock every show you love!";
"reader_log_out_content" = "Are you sure you want to log out?"; "reader_log_out_content" = "Are you sure you want to log out?";
"detail_collect_alert_title" = "Delete This Book?"; "detail_collect_alert_title" = "Delete This Book?";
"detail_collect_alert_text" = "Confirm to delete this book from the list"; "detail_collect_alert_text" = "Confirm to delete this book from the list";
"coins_pack_alert_title" = "Daily Reward is waiting!"; "reader_my_daily_reward" = "Daily Reward is waiting!";
"reader_open_notification" = "Enable Notifications"; "reader_open_notification" = "Enable Notifications";
"reader_open_notification_info" = "Stay informed with popular recommendations and latest updates!"; "reader_open_notification_info" = "Stay informed with popular recommendations and latest updates!";