首次提交

This commit is contained in:
zeng 2025-12-08 16:46:19 +08:00
parent 320d99b66c
commit 5b2b59ba12
496 changed files with 21157 additions and 2 deletions

5
.gitignore vendored
View File

@ -13,6 +13,7 @@ xcuserdata/
*.ipa *.ipa
*.dSYM.zip *.dSYM.zip
*.dSYM *.dSYM
Podfile.lock
## Playgrounds ## Playgrounds
timeline.xctimeline timeline.xctimeline
@ -38,10 +39,10 @@ playground.xcworkspace
# you should judge for yourself, the pros and cons are mentioned at: # you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
# #
# Pods/ Pods/
# #
# Add this line if you want to avoid checking in source code from the Xcode workspace # Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace *.xcworkspace
# Carthage # Carthage
# #

37
Podfile Normal file
View File

@ -0,0 +1,37 @@
# Uncomment the next line to define a global platform for your project
platform :ios, '13.0'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
config.build_settings['EXCLUDED_ARCHITECTURES'] = 'i386'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end
end
target 'ReaderHive' do
use_frameworks!
pod 'Moya'
pod 'SmartCodable', '5.0.15'
pod 'Kingfisher'
pod 'SnapKit'
pod 'YYCategories'
pod 'YYText'
pod 'JXSegmentedView'
pod 'JXPagingView/Paging'
pod 'FSPagerView'
pod 'collection-view-layouts/TagsLayout'
pod 'IQKeyboardManagerSwift'
pod 'LYEmptyView'
pod 'HWPanModal'
pod 'MJRefresh'
pod 'Cosmos' #星星评分
pod 'Toast'
pod 'SVProgressHUD'
pod 'FDFullscreenPopGesture'
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "03980F642ED009E30006E317"
BuildableName = "ReaderHive.app"
BlueprintName = "ReaderHive"
ReferencedContainer = "container:ReaderHive.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "03980F642ED009E30006E317"
BuildableName = "ReaderHive.app"
BlueprintName = "ReaderHive"
ReferencedContainer = "container:ReaderHive.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "03980F642ED009E30006E317"
BuildableName = "ReaderHive.app"
BlueprintName = "ReaderHive"
ReferencedContainer = "container:ReaderHive.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,40 @@
//
// NRDefine.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
///app
let kNRAPPVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0"
let kNRAPPBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0"
let kNRAPPBundleName: String = (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? ""
let kNRAPPName: String = (Bundle.main.infoDictionary!["CFBundleDisplayName"] as? String) ?? ""
#if DEBUG
public func nrPrint(message: Any? , file: String = #file, function: String = #function, line: Int = #line) {
print("\n\(Date(timeIntervalSinceNow: 8 * 60 * 60)) \(file.components(separatedBy: "/").last ?? "") \(function) \(line): \(message ?? "")")
}
#else
public func nrPrint(message: Any?) { }
#endif
public func nr_swizzled_instanceMethod(_ prefix: String, oldClass: Swift.AnyClass!, oldSelector: String, newClass: Swift.AnyClass) {
let newSelector = prefix + "_" + oldSelector;
let originalSelector = NSSelectorFromString(oldSelector)
let swizzledSelector = NSSelectorFromString(newSelector)
let originalMethod = class_getInstanceMethod(oldClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(newClass, swizzledSelector)
let isAdd = class_addMethod(oldClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
if isAdd {
class_replaceMethod(newClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
}else {
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}

View File

@ -0,0 +1,13 @@
//
// NRUserDefaultsKey.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
///token
let kNRLoginTokenDefaultsKey = "kNRLoginTokenDefaultsKey"
///
let kNRUserInfoDefaultsKey = "kNRUserInfoDefaultsKey"
///
let kNRNovelReadSetDefaultsKey = "kNRNovelReadSetDefaultsKey"

View File

@ -0,0 +1,65 @@
//
// CGMutablePath+NRRoundedCorner.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
struct NRRoundedCorner {
var topLeft:CGFloat = 0
var topRight:CGFloat = 0
var bottomLeft:CGFloat = 0
var bottomRight:CGFloat = 0
public static let zero = NRRoundedCorner(topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0)
public init(topLeft: CGFloat, topRight:CGFloat, bottomLeft:CGFloat, bottomRight:CGFloat) {
self.topLeft = topLeft
self.topRight = topRight
self.bottomLeft = bottomLeft
self.bottomRight = bottomRight
}
static func ==(v1:NRRoundedCorner, v2:NRRoundedCorner) -> Bool {
return v1.bottomLeft == v2.bottomLeft
&& v1.bottomRight == v2.bottomRight
&& v1.topLeft == v2.topLeft
&& v1.topRight == v2.topRight
}
static func !=(v1:NRRoundedCorner, v2:NRRoundedCorner) -> Bool {
return !(v1 == v2)
}
}
extension CGMutablePath {
func addRadiusRectangle(_ circulars: NRRoundedCorner, rect: CGRect) {
let minX = rect.minX
let minY = rect.minY
let maxX = rect.maxX
let maxY = rect.maxY
//
let topLeftCenterX = minX + circulars.topLeft
let topLeftCenterY = minY + circulars.topLeft
let topRightCenterX = maxX - circulars.topRight
let topRightCenterY = minY + circulars.topRight
let bottomLeftCenterX = minX + circulars.bottomLeft
let bottomLeftCenterY = maxY - circulars.bottomLeft
let bottomRightCenterX = maxX - circulars.bottomRight
let bottomRightCenterY = maxY - circulars.bottomRight
//
addArc(center: CGPoint(x: topLeftCenterX, y: topLeftCenterY), radius: circulars.topLeft, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3 / 2, clockwise: false)
//
addArc(center: CGPoint(x: topRightCenterX, y: topRightCenterY), radius: circulars.topRight, startAngle: CGFloat.pi * 3 / 2, endAngle: 0, clockwise: false)
//
addArc(center: CGPoint(x: bottomRightCenterX, y: bottomRightCenterY), radius: circulars.bottomRight, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false)
//
addArc(center: CGPoint(x: bottomLeftCenterX, y: bottomLeftCenterY), radius: circulars.bottomLeft, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false)
closeSubpath();
}
}

View File

@ -0,0 +1,36 @@
//
// NSNumber+NRAdd.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/27.
//
import UIKit
extension NSNumber {
func toString(maximumFractionDigits: Int = 10, minimumFractionDigits: Int? = nil, roundingMode: NumberFormatter.RoundingMode? = nil) -> String {
let formatter = NumberFormatter()
formatter.minimumIntegerDigits = 1
formatter.maximumFractionDigits = maximumFractionDigits
if let minimumFractionDigits = minimumFractionDigits {
formatter.minimumFractionDigits = minimumFractionDigits
}
if let roundingMode = roundingMode {
formatter.roundingMode = roundingMode
}
formatter.numberStyle = .none
return formatter.string(from: self) ?? "0"
}
func formattedNumber() -> String {
let num = self.doubleValue
if num > 1000 {
return NSNumber(value: num / 1000).toString(maximumFractionDigits: 1) + "k"
} else {
return self.toString()
}
}
}

View File

@ -0,0 +1,44 @@
//
// String+NRAdd.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import YYCategories
import SmartCodable
extension String: SmartCodable {
var length:Int { return (self as NSString).length }
func size(_ font: UIFont, _ size: CGSize) -> CGSize {
return (self as NSString).size(for: font, size: size, mode: .byWordWrapping)
}
func numberOfLines(_ font: UIFont, _ size: CGSize) -> Int {
let size = self.size(font, size)
let lineHeight = font.lineHeight
return Int(ceil(size.height / lineHeight))
}
}
extension String {
///
func substring(_ range:NSRange) ->String {
return (self as NSString).substring(with: range)
}
///
func matches(_ pattern:String) ->[NSTextCheckingResult] {
if isEmpty {return []}
do {
let regularExpression = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive)
return regularExpression.matches(in: self, options: NSRegularExpression.MatchingOptions.reportProgress, range: NSMakeRange(0, length))
} catch {return []}
}
}

View File

@ -0,0 +1,15 @@
//
// UIFont+NRAdd.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
extension UIFont {
static func font(ofSize: CGFloat, weight: Weight) -> UIFont {
return .systemFont(ofSize: ofSize, weight: weight)
}
}

View File

@ -0,0 +1,53 @@
//
// UINavigationBar+NRAdd.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
extension UINavigationBar {
static let titleFont = UIFont.font(ofSize: 16, weight: .semibold)
static let titleWhiteColor = UIColor.white
static let titleBlackColor = UIColor.black
static func defaultAppearance() -> UINavigationBarAppearance {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithOpaqueBackground()
navBarAppearance.backgroundColor = .clear
navBarAppearance.backgroundEffect = nil
navBarAppearance.shadowColor = UIColor.clear
navBarAppearance.titleTextAttributes = [
NSAttributedString.Key.font : UINavigationBar.titleFont,
NSAttributedString.Key.foregroundColor : UINavigationBar.titleWhiteColor
]
return navBarAppearance
}
}
extension UINavigationBar {
func nr_setTranslucent(isTranslucent: Bool) {
self.isTranslucent = isTranslucent
}
func nr_setBackgroundColor(backgroundColor: UIColor?) {
let appearance = self.standardAppearance
appearance.backgroundColor = backgroundColor
self.standardAppearance = appearance
self.scrollEdgeAppearance = appearance
}
func nr_setTitleTextAttributes(titleTextAttributes: [NSAttributedString.Key : Any]?) {
let appearance = self.standardAppearance
if let titleTextAttributes = titleTextAttributes {
appearance.titleTextAttributes = titleTextAttributes
}
self.scrollEdgeAppearance = appearance
self.standardAppearance = appearance
}
}

View File

@ -0,0 +1,48 @@
//
// UIScreen+NRAdd.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
extension UIScreen {
static var screen: UIScreen {
return UIScreen.main
}
static var width: CGFloat {
return UIScreen.main.bounds.width
}
static var height: CGFloat {
return UIScreen.main.bounds.height
}
static var safeTop: CGFloat {
return NRTool.keyWindow?.safeAreaInsets.top ?? 20
}
static var safeBottom: CGFloat {
return NRTool.keyWindow?.safeAreaInsets.bottom ?? 0
}
static var navBarHeight: CGFloat {
return safeTop + 44
}
static var tabBarHeight: CGFloat {
return safeBottom + 49
}
///
static var widthRatio: CGFloat {
return UIScreen.width / 375
}
static func getRatioWidth(size: CGFloat) -> CGFloat {
return self.widthRatio * size
}
}

View File

@ -0,0 +1,49 @@
//
// UIScrollView+Refresh.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/2.
//
import UIKit
import MJRefresh
extension UIScrollView {
func nr_addRefreshHeader(insetTop: CGFloat = 0, block: (() -> Void)?) {
self.mj_header = MJRefreshNormalHeader(refreshingBlock: {
block?()
})
self.mj_header?.ignoredScrollViewContentInsetTop = insetTop
}
func nr_addRefreshFooter(insetBottom: CGFloat = 0, block: (() -> Void)?) {
self.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: {
block?()
})
self.mj_footer?.ignoredScrollViewContentInsetBottom = insetBottom
}
func nr_endHeaderRefreshing() {
self.mj_header?.endRefreshing()
}
func nr_endFooterRefreshing() {
if self.mj_footer?.state == .noMoreData { return }
self.mj_footer?.endRefreshing()
}
///
func nr_resetNoMoreData() {
self.mj_footer?.resetNoMoreData()
}
func nr_endRefreshingWithNoMoreData() {
self.mj_footer?.endRefreshingWithNoMoreData()
}
}

View File

@ -0,0 +1,21 @@
//
// UIStackView+NRAdd.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
extension UIStackView {
func nr_removeAllArrangedSubview() {
let arrangedSubviews = self.arrangedSubviews
arrangedSubviews.forEach {
self.removeArrangedSubview($0)
$0.removeFromSuperview()
}
}
}

View File

@ -0,0 +1,97 @@
//
// UIView+NRAdd.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
extension UIView {
fileprivate struct AssociatedKeys {
static var nr_roundedCorner: Int?
static var nr_effect: Int?
}
@objc public static func fa_Awake() {
nr_swizzled_instanceMethod("nr", oldClass: self, oldSelector: "layoutSubviews", newClass: self)
}
@objc func nr_layoutSubviews() {
nr_layoutSubviews()
_updateRoundedCorner()
if let effectView = effectView, effectView.frame != self.bounds {
effectView.frame = self.bounds
}
}
}
//MARK: -------------- --------------
extension UIView {
private var roundedCorner: NRRoundedCorner? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.nr_roundedCorner) as? NRRoundedCorner
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.nr_roundedCorner, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
///
func nr_setRoundedCorner(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) {
//
self.roundedCorner = NRRoundedCorner(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
_updateRoundedCorner()
}
private func _updateRoundedCorner() {
guard let roundedCorner = self.roundedCorner else { return }
let rect = self.bounds
let path = CGMutablePath()
path.addRadiusRectangle(roundedCorner, rect: rect)
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds
maskLayer.path = path
self.layer.mask = maskLayer
}
}
//MARK: -------------- --------------
extension UIView {
private var effectView: UIVisualEffectView? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.nr_effect) as? UIVisualEffectView
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.nr_effect, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
///
func nr_addEffectView(style: UIBlurEffect.Style = .light) {
if self.effectView == nil {
let blur = UIBlurEffect(style: style)
let effectView = UIVisualEffectView(effect: blur)
effectView.isUserInteractionEnabled = false
self.addSubview(effectView)
self.sendSubviewToBack(effectView)
self.effectView = effectView
}
}
///
func nr_removeEffectView() {
self.effectView?.removeFromSuperview()
self.effectView = nil
}
}

View File

@ -0,0 +1,44 @@
//
// UserDefaults+NRAdd.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
extension UserDefaults {
static func nr_setObject(_ obj: NSSecureCoding?, forKey key: String) {
let defaults = UserDefaults.standard
guard let obj = obj else {
defaults.removeObject(forKey: key)
return
}
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: obj, requiringSecureCoding: true)
defaults.set(data, forKey: key)
} catch {
print("Error archiving object: \(error)")
}
}
static func nr_object<T: NSObject & NSSecureCoding>(forKey key: String, as type: T.Type) -> T? {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: key) else {
return nil
}
do {
let object = try NSKeyedUnarchiver.unarchivedObject(ofClass: type, from: data)
return object
} catch {
print("Error unarchiving object: \(error)")
return nil
}
}
}

View File

@ -0,0 +1,144 @@
//
// NRHomeAPI.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/2.
//
import UIKit
import Alamofire
struct NRHomeAPI {
///
static func requestHomeData() async -> [NRHomeNovelModuleItem]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/home/all-modules")
param.method = .get
param.isToast = true
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRHomeNovelModuleItem>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
///Explore
static func requestRankingCollection(type: String, days: Int) async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/rankingCollection")
param.method = .get
param.parameters = [
"days" : days,
"type" : type,
"current_page" : 1,
"page_size" : 20,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestHomeNewData(page: Int) async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/newShortPlay")
param.method = .post
param.parameters = [
"current_page" : page,
"page_size" : 20,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestNewData() async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/newShortPlayNoPaginate")
param.method = .post
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestCategoryList() async -> [NRCategoryModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/getCategories")
param.method = .get
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRCategoryModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestCategoryNovel(id: String, page: Int) async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/videoList")
param.method = .get
param.parameters = [
"category_id" : id,
"current_page" : page,
"page_size" : 20
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestSearchNovel(text: String) async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/search")
param.method = .get
param.parameters = [
"search" : text,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
}

View File

@ -0,0 +1,240 @@
//
// NRNovelAPI.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import Alamofire
struct NRNovelAPI {
static func requestDetail(_ id: String) async -> (NRNovelModel?, Int?, String?) {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/novel/getDetails")
param.method = .get
param.parameters = [
"short_play_id" : id,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNovelModel>) in
if response.isSuccess {
continuation.resume(returning:(response.data, response.code, response.msg))
} else {
continuation.resume(returning:(nil, response.code, response.msg))
}
}
}
}
///
static func requestRateScore(_ id: String, stars: CGFloat) async -> Bool {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/novel/rateScore")
param.isLoding = true
param.parameters = [
"short_play_id" : id,
"stars_num" : floor(stars)
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNovelModel>) in
if response.isSuccess {
continuation.resume(returning: true)
} else {
continuation.resume(returning: false)
}
}
}
}
///
static func requestUploadRecord(_ id: String, chapterId: String) {
var param = NRNetwork.Parameters(path: "/novel/watchProgressReport")
param.isToast = false
param.parameters = [
"short_play_id" : id,
"short_play_video_id" : chapterId
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<String>) in
}
}
///
static func requestChapterCatalogList(id: String,
page: Int = 1,
pageSize: Int = 10000,
sort: String = "asc") async -> [NRReadChapterCatalogModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/novel/getChapterList")
param.method = .get
param.parameters = [
"short_play_id" : id,
"order_by" : sort,
"current_page" : page,
"page_size" : pageSize
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRReadChapterCatalogModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
/// 10011 10005
static func requestChapterData(novelId: String, chapterId: String) async -> (NRReadChapterModel?, Int?) {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/novel/getChapterInfo")
param.method = .get
param.parameters = [
"short_play_id" : novelId,
"short_play_video_id" : chapterId,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRReadChapterModel>) in
if response.isSuccess {
continuation.resume(returning: (response.data, response.code))
} else {
continuation.resume(returning: (nil, response.code))
}
}
}
}
static func requestDetailRecommandData() async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/getDetailsRecommand")
param.method = .get
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
@discardableResult
static func requestCollect(isCollect: Bool, id: String, chapterId: String? = nil) async -> Bool {
await withCheckedContinuation { continuation in
let path: String
if isCollect {
path = "/collect"
} else {
path = "/cancelCollect"
}
var param = NRNetwork.Parameters(path: path)
param.method = .post
param.isLoding = true
param.parameters = [
"short_play_id" : id,
"video_id" : chapterId ?? 0
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<String>) in
if response.isSuccess {
continuation.resume(returning: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NotificationCenter.default.post(name: NRNovelAPI.updateCollectStateNotification, object: nil, userInfo: [
"state" : isCollect,
"id" : id,
])
}
} else {
continuation.resume(returning: false)
}
}
}
}
static func requestCollectList(page: Int) async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/myCollections")
param.method = .get
param.parameters = [
"current_page" : page,
"page_size" : 20
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestHistoryList(page: Int) async -> [NRNovelModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/myHistorys")
param.method = .get
param.parameters = [
"current_page" : page,
"page_size" : 20
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRNovelModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestShowRecommendPop(id: String) async -> NRShowRecommendPop? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/novel/isShowRecommendPopUp")
param.method = .get
param.isLoding = true
param.parameters = [
"short_play_id" : id,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRShowRecommendPop>) in
if response.isSuccess {
continuation.resume(returning: response.data)
} else {
continuation.resume(returning: nil)
}
}
}
}
static func requestConfirmRecommend(_ id: String) {
var param = NRNetwork.Parameters(path: "/novel/confirmRecommend")
param.method = .get
param.isLoding = false
param.isToast = false
param.parameters = [
"short_play_id" : id,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<String>) in
}
}
}
extension NRNovelAPI {
/// [ "state" : isCollect, "id" : shortPlayId,]
static let updateCollectStateNotification = Notification.Name(rawValue: "NRNovelAPI.updateCollectStateNotification")
}

View File

@ -0,0 +1,47 @@
//
// NRSettingAPI.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/8.
//
import UIKit
import Alamofire
struct NRSettingAPI {
///
static func requestLanguageList() async -> [NRLanguageModel]? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/languges")
param.method = .get
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRLanguageModel>>) in
if response.isSuccess {
continuation.resume(returning: response.data?.list)
} else {
continuation.resume(returning: nil)
}
}
}
}
///
static func requestLocalizedData(key: String) async -> NRLocalizedModel? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/translates")
param.method = .get
param.parameters = [
"lang_key" : key,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRLocalizedModel>) in
if response.isSuccess {
continuation.resume(returning: response.data)
} else {
continuation.resume(returning: nil)
}
}
}
}
}

View File

@ -0,0 +1,29 @@
//
// NRUserAPI.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import Alamofire
struct NRUserAPI {
static func requestUserInfo() async -> NRUserInfo? {
await withCheckedContinuation { continuation in
var param = NRNetwork.Parameters(path: "/customer/info")
param.method = .get
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRUserInfo>) in
if response.isSuccess {
continuation.resume(returning: response.data)
} else {
continuation.resume(returning: nil)
}
}
}
}
}

View File

@ -0,0 +1,201 @@
//
// NRNetwork.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import Moya
import SmartCodable
class NRNetwork: NSObject {
private static let operationQueue = OperationQueue()
private static var tokenOperation: BlockOperation?
static let provider = MoyaProvider<NRTargetType>()
static func request<T>(parameters: NRNetwork.Parameters, completion: ((_ response: NRNetwork.Response<T>) -> Void)?) {
if NRLoginManager.manager.token == nil {
self.requestToken(completer: nil)
}
if let tokenOperation = self.tokenOperation, parameters.path != "/customer/register" {
let requestOperation = BlockOperation {
let semaphore = DispatchSemaphore(value: 0)
_request(parameters: parameters) { (response: NRNetwork.Response<T>) in
semaphore.signal()
completion?(response)
}
semaphore.wait()
}
///
requestOperation.addDependency(tokenOperation)
operationQueue.addOperation(requestOperation)
} else {
_request(parameters: parameters, completion: completion)
}
}
@discardableResult
static func _request<T>(parameters: NRNetwork.Parameters, completion: ((_ response: NRNetwork.Response<T>) -> Void)?) -> Cancellable {
if parameters.isLoding {
DispatchQueue.main.async {
NRHud.show()
}
}
return provider.request(.request(parameters: parameters)) { result in
if parameters.isLoding {
DispatchQueue.main.async {
NRHud.dismiss()
}
}
guard let completion = completion else {return}
_resultDispose(parameters: parameters, result: result, completion: completion)
}
}
private static func _resultDispose<T>(parameters: NRNetwork.Parameters, result: Result<Moya.Response, MoyaError>, completion: ((_ response: NRNetwork.Response<T>) -> Void)?) {
switch result {
case .success(let response):
let code = response.statusCode
if code == 401 || code == 402 || code == 403 {
if parameters.path == "/customer/register" {
var res = NRNetwork.Response<T>()
res.code = -1
if parameters.isToast {
DispatchQueue.main.async {
NRToast.show(text: "Error".localized)
}
}
completion?(res)
} else {
if code == 402, parameters.isToast {
NRToast.show(text: "network_error_1".localized)
}
//token
self.requestToken { token in
if token != nil {
_Concurrency.Task {
await NRLoginManager.manager.updateUserInfo()
}
}
}
///
if let tokenOperation = self.tokenOperation, parameters.path != "/customer/register" {
let requestOperation = BlockOperation {
let semaphore = DispatchSemaphore(value: 0)
_request(parameters: parameters) { (response: NRNetwork.Response<T>) in
semaphore.signal()
completion?(response)
}
semaphore.wait()
}
///
requestOperation.addDependency(tokenOperation)
operationQueue.addOperation(requestOperation)
}
}
return
}
do {
let tempData = try response.mapString()
nrPrint(message: parameters.parameters)
nrPrint(message: parameters.path)
let response: NRNetwork.Response<T> = _deserialize(data: tempData)
if !response.isSuccess{
if parameters.isToast {
NRToast.show(text: response.msg)
}
}
completion?(response)
} catch {
var res = NRNetwork.Response<T>()
res.code = -1
if parameters.isToast {
DispatchQueue.main.async {
NRToast.show(text: "Error".localized)
}
}
completion?(res)
}
case .failure(let error):
nrPrint(message: error)
var res = NRNetwork.Response<T>()
res.code = -1
if parameters.isToast {
DispatchQueue.main.async {
NRToast.show(text: "network_error_2".localized)
}
}
completion?(res)
break
}
}
///
static private func _deserialize<T>(data: String) -> NRNetwork.Response<T> {
var response: NRNetwork.Response<T>?
let decrypted = NRResponseCryptor.decrypt(data: data)
nrPrint(message: decrypted)
response = NRNetwork.Response<T>.deserialize(from: decrypted)
response?.rawData = decrypted
if let response = response {
return response
} else {
var response = NRNetwork.Response<T>()
response.code = -1
response.msg = "Error".localized
return response
}
}
}
extension NRNetwork {
///token
static func requestToken(completer: ((_ token: NRLoginToken?) -> Void)?) {
guard self.tokenOperation == nil else {
completer?(nil)
return
}
self.tokenOperation = BlockOperation(block: {
let semaphore = DispatchSemaphore(value: 0)
let param = NRNetwork.Parameters(path: "/customer/register")
DispatchQueue.main.async {
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRLoginToken>) in
if let token = response.data {
NRLoginManager.manager.setAccountToken(token)
}
do { semaphore.signal() }
self.tokenOperation = nil
completer?(response.data)
}
}
semaphore.wait()
})
operationQueue.addOperation(self.tokenOperation!)
}
}

View File

@ -0,0 +1,49 @@
//
// NRNetworkModel.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import SmartCodable
import Moya
import Alamofire
extension NRNetwork {
struct Parameters {
var baseURL: URL?
var parameters: [String : Any]?
var method: Moya.Method = .post
var path: String
var isLoding: Bool = false
var isToast: Bool = true
}
struct Response<T : SmartCodable>: SmartCodable {
var code: Int?
var data: T?
var msg: String?
@IgnoredKey
var rawData: Any?
var isSuccess: Bool {
return code == 200
}
}
struct List<T: SmartCodable>: SmartCodable {
var list: [T]?
var pagination: Pagination?
}
struct Pagination: SmartCodable {
var current_page: Int?
var page_size: Int?
var page_total: Int?
var total_size: Int?
}
}

View File

@ -0,0 +1,73 @@
//
// NRNetworkReachableManager.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import Network
class NRNetworkReachableManager: NSObject {
static let manager = NRNetworkReachableManager()
///
var isReachable: Bool?
private var connectionType: NWInterface.InterfaceType?
private var status: NWPath.Status?
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitorQueue")
func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
self.status = path.status
if path.usesInterfaceType(.wifi) {
self.connectionType = .wifi
} else if path.usesInterfaceType(.cellular) {
self.connectionType = .cellular
} else if path.usesInterfaceType(.wiredEthernet) {
self.connectionType = .wiredEthernet
} else {
self.connectionType = nil
}
let agoReachable = self.isReachable
if path.status == .satisfied, self.connectionType != nil {
self.isReachable = true
if agoReachable == false {
DispatchQueue.main.async {
NotificationCenter.default.post(name: NRNetworkReachableManager.networkStatusDidChangeNotification, object: nil)
}
}
} else {
self.isReachable = false
if agoReachable == true {
DispatchQueue.main.async {
NotificationCenter.default.post(name: NRNetworkReachableManager.networkStatusDidChangeNotification, object: nil)
}
}
}
}
monitor.start(queue: queue)
}
func stopMonitoring() {
monitor.cancel()
}
}
extension NRNetworkReachableManager {
@objc static let networkStatusDidChangeNotification = NSNotification.Name(rawValue: "NRNetworkReachableManager.networkStatusDidChangeNotification")
}

View File

@ -0,0 +1,94 @@
//
// NRResponseCryptor.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
struct NRResponseCryptor {
static func decrypt(data: String) -> String {
guard data.hasPrefix("$") else {
return data
}
let decryptedData = deStrBytes(data: data)
return String(data: decryptedData, encoding: .utf8) ?? ""
}
static func deStrBytes(data: String) -> Data {
let hexData = String(data.dropFirst())
var bytes = Data()
var index = hexData.startIndex
while index < hexData.endIndex {
let nextIndex = hexData.index(index, offsetBy: 2, limitedBy: hexData.endIndex) ?? hexData.endIndex
let byteString = String(hexData[index..<nextIndex])
if let byte = UInt8(byteString, radix: 16) {
bytes.append(byte)
}
index = nextIndex
}
return de(data: bytes)
}
//
static func de(data: Data) -> Data {
guard !data.isEmpty else {
return data
}
let saltLen = Int(data[data.startIndex])
guard data.count >= 1 + saltLen else {
return data
}
let salt = data.subdata(in: 1..<1+saltLen)
let encryptedData = data.subdata(in: 1+saltLen..<data.count)
return deWithSalt(data: encryptedData, salt: salt)
}
// 使
static func deWithSalt(data: Data, salt: Data) -> Data {
let decryptedData = cxEd(data: data)
return removeSalt(data: decryptedData, salt: salt)
}
// /
static func cxEd(data: Data) -> Data {
return Data(data.map { $0 ^ 0xFF })
}
//
static func removeSalt(data: Data, salt: Data) -> Data {
guard !salt.isEmpty else {
return data
}
var result = Data()
let saltBytes = [UInt8](salt)
let saltCount = saltBytes.count
for (index, byte) in data.enumerated() {
let saltByte = saltBytes[index % saltCount]
let decryptedByte = calRemoveSalt(v: byte, s: saltByte)
result.append(decryptedByte)
}
return result
}
//
static func calRemoveSalt(v: UInt8, s: UInt8) -> UInt8 {
if v >= s {
return v - s
} else {
return UInt8(0xFF) - (s - v) + 1
}
}
}

View File

@ -0,0 +1,90 @@
//
// NRTargetType.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import SmartCodable
import Moya
import Alamofire
import AdSupport
import YYCategories
enum NRTargetType {
case request(parameters: NRNetwork.Parameters)
}
extension NRTargetType: TargetType {
var baseURL: URL {
return .init(string: NRBaseURL)!
}
var path: String {
switch self {
case .request(let param):
return "/readerhive" + param.path
}
}
var method: Moya.Method {
switch self {
case .request(let param):
return param.method
}
}
var task: Moya.Task {
switch self {
case .request(let param):
let parameters = param.parameters ?? [:]
return .requestParameters(parameters: parameters, encoding: getEncoding())
}
}
var headers: [String : String]? {
var dic: [String : String] = [
"system-version" : UIDevice.current.systemVersion,
"lang-key" : NRLocalizedManager.shared.currentLocalizedKey,
"idfa" : ASIdentifierManager.shared().advertisingIdentifier.uuidString,
"time-zone" : NRTargetType.timeZone(), //
"brand" : "apple", //
"app-version" : kNRAPPVersion,
"app-name" : "ReaderHive",
"device-id" : NRDeviceId.shared.id, //id
"system-type" : "ios",
"model" : UIDevice.current.machineModelName ?? "",
"authorization" : NRLoginManager.manager.token?.token ?? "",
"device-gaid" : UIDevice.current.identifierForVendor?.uuidString ?? "",
"product-prefix" : "ReaderHive"
]
#if DEBUG
dic["security"] = "false"
#endif
return dic
}
}
extension NRTargetType {
var sampleData: Data { return "".data(using: String.Encoding.utf8)! }
func getEncoding() -> ParameterEncoding {
switch self.method {
case .get, .delete:
return URLEncoding.default
default:
return JSONEncoding.default
}
}
static func timeZone() -> String {
let timeZone = NSTimeZone.local as NSTimeZone
let timeZoneSecondsFromGMT = timeZone.secondsFromGMT / 3600
return String(format: "GMT+0%d:00", timeZoneSecondsFromGMT)
}
}

View File

@ -0,0 +1,20 @@
//
// NRUrlPath.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
let NRBaseURL = "https://api-readerhive.readerhive.net"
let NRWebBaseURL = "https://www.readerhive.net"
///
let kNRUserAgreementWebUrl = NRWebBaseURL + "/user_policy"
///
let kNRPrivacyPolicyWebUrl = NRWebBaseURL + "/private"

View File

@ -0,0 +1,44 @@
//
// NRNavigationController.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import FDFullscreenPopGesture
class NRNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
fd_fullscreenPopGestureRecognizer.isEnabled = true
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
if children.count > 0 {
viewController.hidesBottomBarWhenPushed = true
}
super.pushViewController(viewController, animated: animated)
}
override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
for (index, value) in viewControllers.enumerated() {
if index != 0 {
value.hidesBottomBarWhenPushed = true
}
}
super.setViewControllers(viewControllers, animated: animated)
}
override var childForStatusBarStyle: UIViewController? {
return self.topViewController
}
override var childForStatusBarHidden: UIViewController? {
return self.topViewController
}
}

View File

@ -0,0 +1,59 @@
//
// NRTabBarController.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
class NRTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let nav1 = createNavigationController(title: "My List".localized, image: UIImage(named: "tab_bar_icon_01"), selectedImage: UIImage(named: "tab_bar_icon_01_selected"), viewController: NRMyListViewController())
let nav2 = createNavigationController(title: "Home".localized, image: UIImage(named: "tab_bar_icon_02"), selectedImage: UIImage(named: "tab_bar_icon_02_selected"), viewController: NRHomeViewController())
let nav3 = createNavigationController(title: "Explore".localized, image: UIImage(named: "tab_bar_icon_03"), selectedImage: UIImage(named: "tab_bar_icon_03_selected"), viewController: NRExploreViewController())
let nav4 = createNavigationController(title: "Me".localized, image: UIImage(named: "tab_bar_icon_04"), selectedImage: UIImage(named: "tab_bar_icon_04_selected"), viewController: NRMeViewController())
viewControllers = [nav1, nav2, nav3, nav4]
let appearance = UITabBarAppearance()
appearance.backgroundColor = .white
appearance.shadowColor = .clear
appearance.shadowImage = UIImage()
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
.font : UIFont.font(ofSize: 10, weight: .medium),
.foregroundColor : UIColor.black.withAlphaComponent(0.25)
]
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
.font : UIFont.font(ofSize: 10, weight: .medium),
.foregroundColor : UIColor.F_9710_D
]
self.tabBar.standardAppearance = appearance
self.tabBar.scrollEdgeAppearance = appearance
self.tabBar.isTranslucent = false
self.selectedIndex = 1
}
override var childForStatusBarStyle: UIViewController? {
return selectedViewController
}
override var childForStatusBarHidden: UIViewController? {
return selectedViewController
}
private func createNavigationController(title: String, image: UIImage?, selectedImage: UIImage?, viewController: UIViewController) -> UINavigationController {
let nav = NRNavigationController(rootViewController: viewController)
nav.tabBarItem.image = image
nav.tabBarItem.selectedImage = selectedImage
nav.tabBarItem.title = title
return nav
}
}

View File

@ -0,0 +1,123 @@
//
// NRViewController.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import SnapKit
import JXPagingView
import JXSegmentedView
class NRViewController: UIViewController {
var didScrollCallback: ((_ : UIScrollView) -> Void)?
lazy var nr_isEditing = false
lazy var backgroundImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "bg_image_01"))
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = .top
self.view.backgroundColor = .F_8_F_8_F_8
if let navi = navigationController {
if navi.visibleViewController == self {
if navi.viewControllers.count > 1 {
configNavigationBack()
}
}
}
view.addSubview(backgroundImageView)
backgroundImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
}
}
func handleHeaderRefresh(_ completer: (() -> Void)?) {
completer?()
}
func handleFooterRefresh(_ completer: (() -> Void)?) {
completer?()
}
}
extension NRViewController: JXPagingSmoothViewListViewDelegate, JXPagingViewListViewDelegate, JXSegmentedListContainerViewListDelegate {
func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> ()) {
didScrollCallback = callback
}
func listView() -> UIView {
return self.view
}
func listScrollView() -> UIScrollView {
return UIScrollView()
}
func listDidAppear() {
}
func listDidDisappear() {
}
}
extension UIViewController {
func configNavigationBack(_ imageName: String = "arrow_left_icon_01") {
let image = UIImage(named: imageName)
let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(nr_handleNavigationBack))
navigationItem.leftBarButtonItem = leftBarButtonItem
}
@objc func nr_handleNavigationBack() {
self.nr_toLastViewController(animated: true)
}
func nr_toLastViewController(animated: Bool) {
if self.navigationController != nil
{
if self.navigationController?.viewControllers.count == 1
{
self.dismiss(animated: animated, completion: nil)
} else {
self.navigationController?.popViewController(animated: animated)
}
}
else if self.presentingViewController != nil {
self.dismiss(animated: animated, completion: nil)
}
}
}
extension UIViewController {
func nr_setNavigationStyle(backgroundColor: UIColor = .clear,
titleFont: UIFont = UINavigationBar.titleFont,
titleColor: UIColor = UINavigationBar.titleWhiteColor,
isTranslucent: Bool = true
) {
self.navigationController?.navigationBar.nr_setTranslucent(isTranslucent: isTranslucent)
self.navigationController?.navigationBar.nr_setBackgroundColor(backgroundColor: backgroundColor)
self.navigationController?.navigationBar.nr_setTitleTextAttributes(titleTextAttributes: [
NSAttributedString.Key.font : titleFont,
NSAttributedString.Key.foregroundColor : titleColor
])
}
}

View File

@ -0,0 +1,23 @@
//
// NRCollectionView.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
class NRCollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.backgroundColor = .clear
self.contentInsetAdjustmentBehavior = .never
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,44 @@
//
// NRGradientButton.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/5.
//
import UIKit
class NRGradientButton: UIButton {
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
}
}
}

View File

@ -0,0 +1,44 @@
//
// NRGradientView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
class NRGradientView: UIView {
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
}
}
}

View File

@ -0,0 +1,99 @@
//
// NRImageView.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import Kingfisher
class NRImageView: UIImageView {
var placeholderColor = UIColor.E_0_E_0_E_0
var placeholderImage = UIImage(named: "placeholder_image")
private lazy var placeholderImageView: UIImageView = {
let imageView = UIImageView(image: placeholderImage)
imageView.isHidden = true
imageView.contentMode = .scaleAspectFit
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
override init(image: UIImage?) {
super.init(image: image)
_init()
}
override init(image: UIImage?, highlightedImage: UIImage?) {
super.init(image: image, highlightedImage: highlightedImage)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
override func awakeFromNib() {
super.awakeFromNib()
_init()
}
func _init() {
self.contentMode = .scaleAspectFill
self.layer.masksToBounds = true
if image == nil {
self.backgroundColor = self.placeholderColor
placeholderImageView.isHidden = false
}
addSubview(placeholderImageView)
}
override var image: UIImage? {
didSet {
if self.backgroundColor == nil && image == nil {
self.backgroundColor = self.placeholderColor
} else if image != nil {
if self.backgroundColor == self.placeholderColor {
self.backgroundColor = nil
}
}
if image == nil {
placeholderImageView.isHidden = false
} else {
placeholderImageView.isHidden = true
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
placeholderImageView.frame = .init(x: 0, y: 0, width: self.bounds.width * (2 / 3), height: self.bounds.height * (2 / 3))
placeholderImageView.center = .init(x: self.bounds.width / 2, y: self.bounds.height / 2)
}
}
extension UIImageView {
func nr_setImage(_ url: String?, placeholder: UIImage? = nil, completer: ((_ image: UIImage?, _ url: URL?) -> Void)? = nil) {
self.kf.setImage(with: URL(string: url ?? ""), placeholder: placeholder, options: nil) { result in
switch result {
case .success(let value):
completer?(value.image, value.source.url)
default :
completer?(nil, nil)
break
}
}
}
}

View File

@ -0,0 +1,51 @@
//
// NRLabel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
class NRLabel: UILabel {
var textColors: [CGColor]?
var textStartPoint: CGPoint?
var textEndPoint: CGPoint?
override func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
if let text = self.text, text.count > 0, let colors = self.textColors, let startPoint = self.textStartPoint, let endPoine = self.textEndPoint {
self.textColor = UIColor(patternImage: UIImage.nr_getGradientImage(size: size, colors: colors, startPoint: startPoint, endPoint: endPoine))
}
}
}
extension UIImage {
static func nr_getGradientImage(size: CGSize, colors: [CGColor], startPoint: CGPoint, endPoint: CGPoint) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
guard let context = UIGraphicsGetCurrentContext() else{return UIImage()}
let colorSpace = CGColorSpaceCreateDeviceRGB()
///
let gradientRef = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil)!
let startPoint = CGPoint(x: size.width * startPoint.x, y: size.height * startPoint.y)
let endPoint = CGPoint(x: size.width * endPoint.x, y: size.height * endPoint.y)
context.drawLinearGradient(gradientRef, start: startPoint, end: endPoint, options: CGGradientDrawingOptions(arrayLiteral: .drawsBeforeStartLocation,.drawsAfterEndLocation))
let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return gradientImage ?? UIImage()
}
func nr_resized(to size: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: size))
}
}
}

View File

@ -0,0 +1,79 @@
//
// NRPanModalContentView.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/28.
//
import UIKit
import HWPanModal
class NRPanModalContentView: HWPanModalContentView {
var contentHeight = UIScreen.height * (2 / 3)
var mainScrollView: UIScrollView?
///UI contentSize
func setNeedsLayoutUpdate() {
self.panModalSetNeedsLayoutUpdate()
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: HWPanModalPresentable
override func panScrollable() -> UIScrollView? {
return mainScrollView
}
override func longFormHeight() -> PanModalHeight {
return PanModalHeightMake(.content, contentHeight)
}
override func showDragIndicator() -> Bool {
return false
}
override func backgroundConfig() -> HWBackgroundConfig {
let config = HWBackgroundConfig()
config.backgroundAlpha = 0.0
return config
}
///
override func allowsTapBackgroundToDismiss() -> Bool {
return true
}
override func allowsDragToDismiss() -> Bool {
return false
}
override func allowsPullDownWhenShortState() -> Bool {
return false
}
// override func minVerticalVelocityToTriggerDismiss() -> CGFloat {
// return 0
// }
override func showsScrollableVerticalScrollIndicator() -> Bool {
return false
}
override func springDamping() -> CGFloat {
return 1
}
override func cornerRadius() -> CGFloat {
return 16
}
}

View File

@ -0,0 +1,230 @@
//
// NRProgressView.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/29.
//
import UIKit
import YYCategories
import YYText
class NRProgressView: UIView {
///
var panStart: (() -> Void)?
///
var panChange: ((_ progress: CGFloat) -> Void)?
///
var panFinish: ((_ progress: CGFloat) -> Void)?
var progress: CGFloat = 0 {
didSet {
if !isPaning {
setNeedsDisplay()
}
}
}
///
private var tempProgress: CGFloat = 0
///
private var panProgress: CGFloat = 0
var progressColor: UIColor = .black.withAlphaComponent(0.25)
var currentProgress: UIColor = .F_9710_D
var lineWidth: CGFloat = 4
///
var isLoading = false {
didSet {
if isLoading {
if gradientTimer == nil {
gradientTimer = Timer.scheduledTimer(timeInterval: 0.05, target: YYTextWeakProxy(target: self), selector: #selector(handleGradientTimer), userInfo: nil, repeats: true)
}
} else {
gradientTimer?.invalidate()
gradientTimer = nil
}
}
}
var thumbImage: UIImage?
var insets: UIEdgeInsets = .init(top: 0, left: 15, bottom: 0, right: 15) {
didSet {
self.invalidateIntrinsicContentSize()
setNeedsDisplay()
}
}
private(set) lazy var panGesture: UIPanGestureRecognizer = {
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
return pan
}()
private(set) lazy var tagGesture: UITapGestureRecognizer = {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:)))
return tap
}()
///
private var isPaning: Bool = false
private var gradientTimer: Timer?
private var gradientValue: CGFloat = 0
override var intrinsicContentSize: CGSize {
return .init(width: UIScreen.width, height: lineWidth + insets.top + insets.bottom)
}
override init(frame: CGRect) {
super.init(frame: frame)
// self.backgroundColor = progressColor
self.backgroundColor = .clear
self.addGestureRecognizer(panGesture)
self.addGestureRecognizer(tagGesture)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setNeedsDisplay()
}
@objc private func handleGradientTimer() {
gradientValue += 0.1
if gradientValue > 1 {
gradientValue = 0
}
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
let width = rect.width
let thumbImageSize = self.thumbImage?.size ?? .zero
let progressX = insets.left + thumbImageSize.width / 2
let progressY = insets.top
let progressWidth = width - insets.left - insets.right - thumbImageSize.width
if isLoading, !isPaning {
//
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colors: [CGColor] = [
UIColor.clear.cgColor,
UIColor.white.cgColor,
UIColor.clear.cgColor
]
let locations: [CGFloat] = [0.0, gradientValue, 1.0]
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations) else {
return
}
let gradientRect = CGRect(x: progressX,
y: progressY,
width: progressWidth,
height: lineWidth)
//
let startPoint = CGPoint(x: rect.minX, y: rect.minY)
let endPoint = CGPoint(x: rect.maxX, y: rect.maxY)
//
context.saveGState()
context.clip(to: gradientRect)
//
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])
} else {
var progress = self.progress
if self.isPaning {
progress = self.panProgress
}
if progress < 0 {
progress = 0
}
if progress > 1 {
progress = 1
}
///
let progressPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth, height: lineWidth), cornerRadius: lineWidth / 2)
context.addPath(progressPath.cgPath)
context.setFillColor(progressColor.cgColor)
context.fillPath()
///
let currentPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth * progress, height: lineWidth), cornerRadius: lineWidth / 2)
context.addPath(currentPath.cgPath)
context.setFillColor(currentProgress.cgColor)
context.fillPath()
if let thumbImage = thumbImage {
let size = thumbImage.size
let frame = CGRect(x: progressWidth * progress - size.width / 2 + progressX, y: progressY - size.width / 2 + lineWidth / 2, width: size.width, height: size.height)
thumbImage.draw(in: frame)
}
}
}
}
extension NRProgressView {
@objc func handlePanGesture(sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
self.isPaning = true
self.tempProgress = self.progress
sender.setTranslation(CGPoint(x: 0, y: 0), in: self)
self.panStart?()
case .changed:
let point = sender.translation(in: self)
let offsetX = point.x / (self.width - self.insets.left - self.insets.right)
self.panProgress = self.tempProgress + offsetX
if self.panProgress < 0 {
self.panProgress = 0
}
if self.panProgress > 1 {
self.panProgress = 1
}
self.panChange?(self.panProgress)
setNeedsDisplay()
default:
self.isPaning = false
self.panFinish?(self.panProgress)
self.panProgress = 0
}
}
@objc func handleTapGesture(sender: UITapGestureRecognizer) {
let point = sender.location(in: self)
let offsetX = (point.x - self.insets.left) / (self.width - self.insets.left - self.insets.right)
self.panFinish?(offsetX)
}
}

View File

@ -0,0 +1,21 @@
//
// NRScrollView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
class NRScrollView: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
self.contentInsetAdjustmentBehavior = .never
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}

View File

@ -0,0 +1,56 @@
//
// NRTableView.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/25.
//
import UIKit
class NRTableView: UITableView, UIGestureRecognizerDelegate {
var shouldRecognizeSimultaneously = false
var insetGroupedMargins: CGFloat = 16
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
separatorInset = .init(top: 0, left: 16, bottom: 0, right: 16)
self.backgroundColor = .clear
self.contentInsetAdjustmentBehavior = .never
if style == .insetGrouped {
sectionFooterHeight = 14
sectionHeaderHeight = 0.1
} else if style == .plain {
if #available(iOS 15.0, *) {
sectionHeaderTopPadding = 0
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var layoutMargins: UIEdgeInsets {
set {
super.layoutMargins = newValue
}
get {
var margins = super.layoutMargins
if self.style == .insetGrouped {
margins.left = self.safeAreaInsets.left + insetGroupedMargins
margins.right = self.safeAreaInsets.right + insetGroupedMargins
}
return margins
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return shouldRecognizeSimultaneously
}
}

View File

@ -0,0 +1,49 @@
//
// NRTableViewCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/25.
//
import UIKit
class NRTableViewCell: UITableViewCell {
private(set) lazy var nr_indicatorImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "arrow_right_icon_05"))
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.layer.rasterizationScale = UIScreen.main.scale
self.layer.shouldRasterize = true
self.selectionStyle = .none
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension UITableViewCell {
var br_tableView: UITableView? {
return self.value(forKey: "_tableView") as? UITableView
}
}

View File

@ -0,0 +1,157 @@
//
// NRWebView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
@preconcurrency import WebKit
import YYText
//MARK:-------------- VPWebViewDelegate --------------
@objc protocol NRWebViewDelegate: NSObjectProtocol {
@objc optional func nr_webView(_ webView: NRWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool
@objc optional func nr_webViewDidStartLoad(_ webView: NRWebView)
@objc optional func nr_webViewDidFinishLoad(_ webView: NRWebView)
@objc optional func nr_webView(_ webView: NRWebView, didFailLoadWithError error: Error)
///
@objc optional func nr_webView(webView: NRWebView, didChangeProgress progress: CGFloat)
///
@objc optional func nr_webView(webView: NRWebView, didChangeTitle title: String)
///web
@objc optional func nr_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
}
class NRWebView: WKWebView {
weak var delegate: NRWebViewDelegate?
private(set) var scriptMessageHandlerArray: [String] = [
kNRWebMessageAPP,
kNRWebMessageOpenFeedbackList,
kNRWebMessageOpenFeedbackDetail,
kNRWebMessageOpenPhotoPicker,
kNRWebMessageAccountDeletionFinish,
]
deinit {
self.removeObserver(self, forKeyPath: "estimatedProgress")
self.removeObserver(self, forKeyPath: "title")
}
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
addScriptMessageHandler()
_setupInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func _setupInit() {
self.isOpaque = false
self.navigationDelegate = self
self.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
self.addObserver(self, forKeyPath: "title", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as? NRWebView == self {
if keyPath == "estimatedProgress", let progress = change?[NSKeyValueChangeKey.newKey] as? CGFloat {
self.delegate?.nr_webView?(webView: self, didChangeProgress: progress)
} else if keyPath == "title", let title = change?[NSKeyValueChangeKey.newKey] as? String {
self.delegate?.nr_webView?(webView: self, didChangeTitle: title)
}
}
}
func load(urlStr: String) {
guard let url = URL(string: urlStr) else { return }
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30)
self.load(request)
}
func removeScriptMessageHandler() {
self.scriptMessageHandlerArray.forEach{
configuration.userContentController.removeScriptMessageHandler(forName: $0)
}
}
func addScriptMessageHandler() {
self.scriptMessageHandlerArray.forEach{
configuration.userContentController.add(YYTextWeakProxy(target: self) as! WKScriptMessageHandler, name: $0)
}
}
}
//MARK:-------------- WKNavigationDelegate --------------
extension NRWebView: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
decisionHandler(.allow);
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url,
url.scheme != "http",
url.scheme != "https"
{
UIApplication.shared.open(url)
decisionHandler(.cancel)
return
}
if let result = self.delegate?.nr_webView?(self, shouldStartLoadWith: navigationAction) {
if result {
decisionHandler(.allow)
} else {
decisionHandler(.cancel)
}
} else {
decisionHandler(.allow)
}
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
self.delegate?.nr_webViewDidStartLoad?(self)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.delegate?.nr_webViewDidFinishLoad?(self)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
self.delegate?.nr_webView?(self, didFailLoadWithError: error)
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
self.delegate?.nr_webView?(self, didFailLoadWithError: error)
}
}
//MARK:-------------- WKScriptMessageHandler --------------
extension NRWebView: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
self.delegate?.nr_userContentController?(userContentController, didReceive: message)
}
}

View File

@ -0,0 +1,28 @@
//
// NRWebViewController+Script.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import WebKit
///APP
let kNRWebMessageAPP = "js2app"
///
let kNRWebMessageOpenFeedbackList = "openFeedbackList"
///
let kNRWebMessageOpenFeedbackDetail = "openFeedbackDetail"
///
let kNRWebMessageOpenPhotoPicker = "openPhotoPicker"
///
let kNRWebMessageAccountDeletionFinish = "accountLogout"
extension NRWebViewController {
func nr_webViewUserContentController(didReceive message: WKScriptMessage) {
}
}

View File

@ -0,0 +1,111 @@
//
// NRWebViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import WebKit
import SnapKit
class NRWebViewController: NRViewController {
var webUrl: String?
var needAutoRefresh = true
private(set) lazy var webView: NRWebView = {
let controller = WKUserContentController()
let config = WKWebViewConfiguration()
config.userContentController = controller
config.preferences.javaScriptEnabled = true
/** JS */
config.preferences.javaScriptCanOpenWindowsAutomatically = true
let webView = NRWebView(frame: self.view.bounds, configuration: config)
webView.delegate = self
return webView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = .top
self.backgroundImageView.isHidden = true
configNavigationBack("arrow_left_icon_05")
_setupUI()
if let url = webUrl {
self.load(webUrl: url)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
func load(webUrl: String) {
let str: String = webUrl
guard let url = URL(string: str) else { return }
let request = URLRequest(url: url, timeoutInterval: 30)
self.webView.load(request)
}
func reload() {
self.webView.reload()
}
}
extension NRWebViewController {
private func _setupUI() {
self.view.addSubview(webView)
self.webView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(0)
make.right.equalToSuperview().offset(0)
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
make.bottom.equalToSuperview().offset(0)
}
}
}
//MARK: -------------- VPWebViewDelegate --------------
extension NRWebViewController: NRWebViewDelegate {
func nr_webView(_ webView: NRWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool {
self.webView.isHidden = false
return true
}
func nr_webViewDidStartLoad(_ webView: NRWebView) {
NRHud.show(containerView: self.view)
}
func nr_webView(webView: NRWebView, didChangeTitle title: String) {
}
func nr_webViewDidFinishLoad(_ webView: NRWebView) {
self.webView.isHidden = false
NRHud.dismiss()
}
func nr_webView(_ webView: NRWebView, didFailLoadWithError error: any Error) {
NRHud.dismiss()
}
func nr_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
nr_webViewUserContentController(didReceive: message)
}
}

View File

@ -0,0 +1,47 @@
//
// NRExploreNovelMenuItem.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/25.
//
import UIKit
struct NRExploreNovelMenuItem {
enum ItemType: String {
case genres
case trending = "trending"
case topRated = "score"
case collected = "collect"
case bestSellers = "sellers"
// func getTitle() -> String {
// switch self {
// case .genres:
// return "Genres"
//
// case .trending:
// return "Trending".localized
//
// case .topRated:
// return "Top Rated".localized
//
// case .collected:
// return "Most Collected".localized
//
// case .bestSellers:
// return "Best Sellers".localized
//
// default:
// return ""
// }
// }
}
var type: ItemType?
var title: String?
var icon: UIImage?
var selectedIcon: UIImage?
}

View File

@ -0,0 +1,199 @@
//
// NRExploreNovelContentListCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import SnapKit
class NRExploreNovelContentListCell: UICollectionViewCell {
var row: Int = 0 {
didSet {
let num = row + 1
numLabel.text = "\(num)"
if num < 4 {
numLabel.textColor = .white
} else {
numLabel.textColor = .black.withAlphaComponent(0.25)
}
switch num {
case 1:
numBgView.image = UIImage(named: "num_1_bg_icon")
case 2:
numBgView.image = UIImage(named: "num_2_bg_icon")
case 3:
numBgView.image = UIImage(named: "num_3_bg_icon")
default:
numBgView.image = UIImage(named: "num_4_bg_icon")
}
}
}
var menuItem: NRExploreNovelMenuItem? {
didSet {
switch menuItem?.type {
case .trending:
countView.configuration?.image = UIImage(named: "hot_icon_01")
case .topRated:
countView.configuration?.image = UIImage(named: "star_icon_01")
case .collected:
countView.configuration?.image = UIImage(named: "collected_icon_01")
default:
break
}
if menuItem?.type == .bestSellers {
countView.isHidden = true
} else {
countView.isHidden = false
countView.setNeedsUpdateConfiguration()
}
}
}
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
nameLabel.text = model?.name
categoryLabel.text = model?.category?.first
countView.setNeedsUpdateConfiguration()
}
}
private var grade: CGFloat {
switch menuItem?.type {
case .trending:
return CGFloat(model?.watch_total ?? 0)
case .topRated:
return model?.rate ?? 0
case .collected:
return CGFloat(model?.collect_total ?? 0)
default:
return 0
}
}
lazy var numBgView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
lazy var numLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .medium)
return label
}()
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
return imageView
}()
lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .medium)
label.textColor = .black
label.numberOfLines = 2
return label
}()
lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.25)
return label
}()
lazy var countView: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.contentInsets = .zero
configuration.imagePadding = 4
configuration.imagePlacement = .top
let button = UIButton(configuration: configuration)
button.isUserInteractionEnabled = false
button.configurationUpdateHandler = { [weak self] button in
guard let self = self else { return }
button.configuration?.attributedTitle = AttributedString(NSNumber(value: self.grade).formattedNumber(), attributes: AttributeContainer([
.font : UIFont.font(ofSize: 10, weight: .regular),
.foregroundColor : UIColor.black
]))
}
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
nameLabel.text = "Shadows of Vengeance"
categoryLabel.text = "Satisfying"
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRExploreNovelContentListCell {
private func nr_setupUI() {
contentView.addSubview(numBgView)
numBgView.addSubview(numLabel)
contentView.addSubview(coverImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(categoryLabel)
contentView.addSubview(countView)
numBgView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.centerX.equalTo(self.contentView.snp.left).offset(20)
}
numLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(-2)
}
coverImageView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.left.equalTo(numBgView.snp.right).offset(8)
make.width.equalTo(40)
}
nameLabel.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(8)
make.centerY.equalTo(self.contentView.snp.top).offset(20)
make.right.lessThanOrEqualToSuperview().offset(-60)
}
categoryLabel.snp.makeConstraints { make in
make.left.equalTo(nameLabel)
make.top.equalTo(nameLabel.snp.bottom).offset(5)
}
countView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.centerX.equalTo(self.contentView.snp.right).offset(-32)
}
}
}

View File

@ -0,0 +1,40 @@
//
// NRExploreNovelGenresCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import SnapKit
class NRExploreNovelGenresCell: UICollectionViewCell {
lazy var textLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.layer.cornerRadius = 8
self.contentView.layer.masksToBounds = true
self.contentView.backgroundColor = .F_2_EFEE
contentView.addSubview(textLabel)
textLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(-5)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,73 @@
//
// NRExploreNovelMenuCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/25.
//
import UIKit
import SnapKit
class NRExploreNovelMenuCell: UICollectionViewCell {
var item: NRExploreNovelMenuItem? {
didSet {
titleLabel.text = item?.title
updateState()
}
}
var nr_isSelected: Bool = false {
didSet {
updateState()
}
}
lazy var iconImageView = UIImageView()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.layer.cornerRadius = 8
contentView.layer.masksToBounds = true
contentView.layer.borderWidth = 1
addSubview(iconImageView)
addSubview(titleLabel)
iconImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(6)
}
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(iconImageView.snp.bottom).offset(8)
make.right.lessThanOrEqualToSuperview().offset(-2)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateState() {
iconImageView.image = nr_isSelected ? item?.selectedIcon : item?.icon
titleLabel.textColor = nr_isSelected ? .black : .black.withAlphaComponent(0.25)
if nr_isSelected {
contentView.backgroundColor = .white
contentView.layer.borderColor = UIColor.F_2_EFEE.cgColor
} else {
contentView.backgroundColor = .clear
contentView.layer.borderColor = UIColor.clear.cgColor
}
}
}

View File

@ -0,0 +1,101 @@
//
// NRExploreNovelMenuView.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/25.
//
import UIKit
import SnapKit
class NRExploreNovelMenuView: UIView {
override var intrinsicContentSize: CGSize {
return .init(width: 60, height: UIScreen.height)
}
var didSelected: ((Int) -> Void)?
weak var viewModel: NRExploreNovelViewModel?
private(set) var selectedIndex = 0
private lazy var collectionViewLayout: NRWaterfallFlowLayout = {
let layout = NRWaterfallFlowLayout()
layout.delegate = self
return layout
}()
private lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsVerticalScrollIndicator = false
collectionView.register(NRExploreNovelMenuCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRExploreNovelMenuView {
private func nr_setupUI() {
addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
//AMRK: UICollectionViewDelegate UICollectionViewDataSource
extension NRExploreNovelMenuView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRExploreNovelMenuCell
cell.item = self.viewModel?.menuDataArr[indexPath.row]
cell.nr_isSelected = indexPath.row == self.selectedIndex
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.viewModel?.menuDataArr.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard self.selectedIndex != indexPath.row else { return }
self.selectedIndex = indexPath.row
self.collectionView.reloadData()
self.didSelected?(self.selectedIndex)
}
}
//MARK: NRWaterfallMutiSectionDelegate
extension NRExploreNovelMenuView: NRWaterfallMutiSectionDelegate {
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: NRWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
let item = self.viewModel?.menuDataArr[indexPath.row]
let textHeight = item?.title?.size(.font(ofSize: 10, weight: .regular), .init(width: 60, height: 100)).height ?? 0
return textHeight + 44
}
func columnNumber(collectionView collection: UICollectionView, layout: NRWaterfallFlowLayout, section: Int) -> Int {
return 1
}
func lineSpacing(collectionView collection: UICollectionView, layout: NRWaterfallFlowLayout, section: Int) -> CGFloat {
return 16
}
}

View File

@ -0,0 +1,108 @@
//
// NRNovelGenresCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import SnapKit
class NRNovelGenresCell: UICollectionViewCell {
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
titleLabel.text = model?.name
if let text = model?.categoryList?.first?.name, text.count > 0 {
self.categoryView.isHidden = false
self.categoryView.text = text
} else if let text = model?.category?.first, text.count > 0 {
self.categoryView.isHidden = false
self.categoryView.text = text
} else {
self.categoryView.isHidden = true
}
if model?.tag_type == .new {
markImageView.isHidden = false
markImageView.image = UIImage(named: "new_icon_01")
} else if model?.tag_type == .hot {
markImageView.isHidden = false
markImageView.image = UIImage(named: "hot_icon_03")
} else {
markImageView.isHidden = true
}
}
}
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
label.numberOfLines = 2
return label
}()
lazy var categoryView: NRHomeCategoryTagView = {
let view = NRHomeCategoryTagView()
return view
}()
lazy var markImageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
categoryView.text = "Satisfying"
titleLabel.text = "Vanished Without a Word"
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRNovelGenresCell {
private func nr_setupUI() {
contentView.addSubview(coverImageView)
coverImageView.addSubview(markImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(categoryView)
coverImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-68)
}
markImageView.snp.makeConstraints { make in
make.left.top.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
make.top.equalTo(coverImageView.snp.bottom).offset(8)
}
categoryView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.bottom.equalToSuperview()
}
}
}

View File

@ -0,0 +1,109 @@
//
// NRExploreNovelContentListViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import SnapKit
class NRExploreNovelContentListViewController: NRViewController {
var menuItem: NRExploreNovelMenuItem?
var contentType: NRExploreNovelContentViewController.ContentType = .today
lazy var dataArr: [NRNovelModel] = []
lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(60)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 8
section.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
let layout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.contentInset = .init(top: 0, left: 0, bottom: 10, right: 0)
collectionView.register(NRExploreNovelContentListCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundImageView.isHidden = true
self.view.backgroundColor = .clear
nr_setupUI()
Task {
await requestDataArr()
}
}
}
extension NRExploreNovelContentListViewController {
private func nr_setupUI() {
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(12)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRExploreNovelContentListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRExploreNovelContentListCell
cell.menuItem = self.menuItem
cell.row = indexPath.row
cell.model = dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.row]
let vc = NRNovelDetailViewController()
vc.novelId = model.id ?? ""
self.navigationController?.pushViewController(vc, animated: true)
}
}
extension NRExploreNovelContentListViewController {
private func requestDataArr() async {
guard let type = self.menuItem?.type else { return }
guard let list = await NRHomeAPI.requestRankingCollection(type: type.rawValue, days: self.contentType.rawValue) else { return }
self.dataArr = list
self.collectionView.reloadData()
}
}

View File

@ -0,0 +1,166 @@
//
// NRExploreNovelContentViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import SnapKit
import JXSegmentedView
class NRExploreNovelContentViewController: NRViewController {
enum ContentType: Int {
case today = 1
case week = 7
case month = 30
}
var menuItem: NRExploreNovelMenuItem?
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
return label
}()
lazy var moreButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
}))
button.setImage(UIImage(named: "arrow_right_icon_03"), for: .normal)
return button
}()
lazy var pageMenuDataSource: JXSegmentedTitleDataSource = {
let dataSource = NRExploreNovelMenuDataSource()
dataSource.titles = ["Today".localized, "This Week".localized, "This Month".localized]
dataSource.titleNormalFont = .font(ofSize: 12, weight: .regular)
dataSource.titleSelectedFont = .font(ofSize: 12, weight: .semibold)
dataSource.titleNormalColor = .black.withAlphaComponent(0.5)
dataSource.titleSelectedColor = .white
dataSource.itemSpacing = 12
dataSource.itemWidthIncrement = 24
dataSource.isTitleColorGradientEnabled = true
return dataSource
}()
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = .F_2_EFEE
return view
}()
lazy var pageMenuView: JXSegmentedView = {
let view = JXSegmentedView()
view.dataSource = pageMenuDataSource
view.contentEdgeInsetLeft = 12
view.contentEdgeInsetRight = 12
view.listContainer = pageView
view.indicators = [SegmentedIndicatorView()]
return view
}()
lazy var pageView: JXSegmentedListContainerView = {
let pageView = JXSegmentedListContainerView(dataSource: self)
return pageView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundImageView.isHidden = true
self.view.backgroundColor = .clear
self.titleLabel.text = menuItem?.title
nr_setupUI()
}
}
extension NRExploreNovelContentViewController {
private func nr_setupUI() {
view.addSubview(titleLabel)
view.addSubview(moreButton)
view.addSubview(pageMenuView)
view.addSubview(pageView)
view.addSubview(lineView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.centerY.equalTo(moreButton)
}
moreButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-12)
make.top.equalToSuperview().offset(16)
}
pageMenuView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(moreButton.snp.bottom).offset(12)
make.height.equalTo(24)
}
lineView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.right.equalToSuperview().offset(-12)
make.top.equalTo(pageMenuView.snp.bottom).offset(12)
make.height.equalTo(1)
}
pageView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(lineView.snp.bottom)
}
}
}
extension NRExploreNovelContentViewController: JXSegmentedListContainerViewDataSource {
func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int {
return pageMenuDataSource.titles.count
}
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> any JXSegmentedListContainerViewListDelegate {
let vc = NRExploreNovelContentListViewController()
vc.menuItem = self.menuItem
if index == 0 {
vc.contentType = .today
} else if index == 1 {
vc.contentType = .week
} else if index == 2 {
vc.contentType = .month
}
return vc
}
}
extension NRExploreNovelContentViewController {
class SegmentedIndicatorView: JXSegmentedIndicatorLineView {
override func commonInit() {
super.commonInit()
indicatorColor = .F_9710_D
indicatorHeight = JXSegmentedViewAutomaticDimension
}
}
}

View File

@ -0,0 +1,104 @@
//
// NRExploreNovelGenresViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import SnapKit
class NRExploreNovelGenresViewController: NRViewController {
lazy var dataArr: [NRCategoryModel] = []
lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(48)), subitems: [item])
group.interItemSpacing = .fixed(8)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 8
section.contentInsets = .init(top: 0, leading: 12, bottom: 0, trailing: 12)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
let layout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.contentInset = .init(top: 0, left: 0, bottom: 10, right: 0)
collectionView.register(NRExploreNovelGenresCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundImageView.isHidden = true
self.view.backgroundColor = .clear
nr_setupUI()
Task {
await requestDataArr()
}
}
}
extension NRExploreNovelGenresViewController {
private func nr_setupUI() {
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(16)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRExploreNovelGenresViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRExploreNovelGenresCell
cell.textLabel.text = dataArr[indexPath.row].name
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = NRNovelGenresViewController()
vc.model = self.dataArr[indexPath.row]
self.navigationController?.pushViewController(vc, animated: true)
}
}
extension NRExploreNovelGenresViewController {
private func requestDataArr() async {
guard let list = await NRHomeAPI.requestCategoryList() else { return }
self.dataArr = list
self.collectionView.reloadData()
}
}

View File

@ -0,0 +1,109 @@
//
// NRExploreNovelViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/25.
//
import UIKit
import SnapKit
//import JXPagingView
import JXSegmentedView
class NRExploreNovelViewController: NRViewController {
lazy var viewModel = NRExploreNovelViewModel()
lazy var menuView: NRExploreNovelMenuView = {
let view = NRExploreNovelMenuView()
view.viewModel = self.viewModel
view.didSelected = { [weak self] index in
guard let self = self else { return }
let contentScrollView = self.pageView.contentScrollView()
contentScrollView.setContentOffset(CGPoint(x: contentScrollView.bounds.size.width*CGFloat(index), y: 0), animated: false)
self.pageView.didClickSelectedItem(at: index)
}
return view
}()
lazy var contentView: UIView = {
let view = UIView()
view.layer.cornerRadius = 16
view.layer.masksToBounds = true
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.F_2_EFEE.cgColor
view.backgroundColor = .white
return view
}()
lazy var pageView: JXSegmentedListContainerView = {
let view = JXSegmentedListContainerView(dataSource: self)
view.listCellBackgroundColor = .clear
view.contentScrollView().isScrollEnabled = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundImageView.isHidden = true
self.view.backgroundColor = .clear
nr_setupUI()
}
}
extension NRExploreNovelViewController {
private func nr_setupUI() {
view.addSubview(menuView)
view.addSubview(contentView)
contentView.addSubview(pageView)
menuView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.top.equalTo(menuView)
make.left.equalTo(menuView.snp.right).offset(8)
make.right.equalToSuperview().offset(-16)
make.bottom.equalToSuperview().offset(UIScreen.tabBarHeight)
}
pageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-UIScreen.tabBarHeight)
}
}
}
//MARK: JXSegmentedListContainerViewDataSource
extension NRExploreNovelViewController: JXSegmentedListContainerViewDataSource {
func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int {
return self.viewModel.menuDataArr.count
}
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> any JXSegmentedListContainerViewListDelegate {
let item = self.viewModel.menuDataArr[index]
switch item.type {
case .genres:
return NRExploreNovelGenresViewController()
default:
let vc = NRExploreNovelContentViewController()
vc.menuItem = item
return vc
}
}
}

View File

@ -0,0 +1,69 @@
//
// NRExploreViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/25.
//
import UIKit
import SnapKit
class NRExploreViewController: NRViewController {
lazy var searchButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "search_icon_01"), for: .normal)
button.addAction(UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let vc = NRSearchViewController()
self.navigationController?.pushViewController(vc, animated: true)
}), for: .touchUpInside)
return button
}()
lazy var titleView = UIImageView(image: UIImage(named: "home_title_image"))
lazy var novelVC = NRExploreNovelViewController()
override func viewDidLoad() {
super.viewDidLoad()
nr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
extension NRExploreViewController {
private func nr_setupUI() {
view.addSubview(searchButton)
view.addSubview(titleView)
addChild(novelVC)
view.addSubview(novelVC.view)
searchButton.snp.makeConstraints { make in
make.height.equalTo(44)
make.right.equalToSuperview().offset(-16)
make.top.equalTo(UIScreen.safeTop)
}
titleView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalTo(searchButton)
}
novelVC.view.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(searchButton.snp.bottom)
}
}
}

View File

@ -0,0 +1,144 @@
//
// NRNovelGenresViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import SnapKit
import LYEmptyView
class NRNovelGenresViewController: NRViewController {
var model: NRCategoryModel?
private lazy var dataArr: [NRNovelModel] = []
private lazy var page = 1
lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let itemWidth = (UIScreen.width - 32 - 40) / 3
let itemHeight = 150 / 100 * itemWidth + 68
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1 / 3), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(itemHeight)), subitems: [item])
group.interItemSpacing = .fixed(20)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 18
section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
let layout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.ly_emptyView = NREmpty.nr_emptyView()
collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
collectionView.nr_addRefreshHeader { [weak self] in
self?.handleHeaderRefresh(nil)
}
collectionView.nr_addRefreshFooter(insetBottom: collectionView.contentInset.bottom) { [weak self] in
self?.handleFooterRefresh(nil)
}
collectionView.register(NRNovelGenresCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = .top
self.backgroundImageView.isHidden = true
self.title = self.model?.name
configNavigationBack("arrow_left_icon_05")
nr_setupUI()
Task {
await requestDataArr(page: 1)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: 1)
self.collectionView.nr_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: self.page + 1)
self.collectionView.nr_endFooterRefreshing()
}
}
}
extension NRNovelGenresViewController {
private func nr_setupUI() {
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight + 10)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRNovelGenresViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRNovelGenresCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.row]
let vc = NRNovelDetailViewController()
vc.novelId = model.id ?? ""
self.navigationController?.pushViewController(vc, animated: true)
}
}
extension NRNovelGenresViewController {
private func requestDataArr(page: Int) async {
guard let id = self.model?.id else { return }
guard let list = await NRHomeAPI.requestCategoryNovel(id: id, page: page) else { return }
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.collectionView.reloadData()
}
}

View File

@ -0,0 +1,53 @@
//
// NRExploreNovelMenuDataSource.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
import JXSegmentedView
class NRExploreNovelMenuDataSource: JXSegmentedTitleDataSource {
nonisolated override init() {
super.init()
}
nonisolated override func registerCellClass(in segmentedView: JXSegmentedView) {
MainActor.assumeIsolated {
segmentedView.collectionView.register(MenuCell.self, forCellWithReuseIdentifier: "MenuCell")
}
}
nonisolated override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
return MainActor.assumeIsolated {
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "MenuCell", at: index)
return cell
}
}
}
extension NRExploreNovelMenuDataSource {
class MenuCell: JXSegmentedTitleCell {
override func commonInit() {
super.commonInit()
self.contentView.backgroundColor = .black.withAlphaComponent(0.05)
self.contentView.layer.cornerRadius = 12
self.contentView.layer.masksToBounds = true
}
override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
super.reloadData(itemModel: itemModel, selectedType: selectedType)
}
}
}

View File

@ -0,0 +1,24 @@
//
// NRExploreNovelViewModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/11/26.
//
import UIKit
class NRExploreNovelViewModel: NSObject {
lazy var menuDataArr: [NRExploreNovelMenuItem] = {
let arr = [
NRExploreNovelMenuItem(type: .genres, title: "Genres".localized, icon: UIImage(named: "explore_genres_icon_01"), selectedIcon: UIImage(named: "explore_genres_icon_01_selected")),
NRExploreNovelMenuItem(type: .trending, title: "Trending".localized, icon: UIImage(named: "explore_trending_icon_01"), selectedIcon: UIImage(named: "explore_trending_icon_01_selected")),
NRExploreNovelMenuItem(type: .topRated, title: "Top Rated".localized, icon: UIImage(named: "explore_top_rated_icon_01"), selectedIcon: UIImage(named: "explore_top_rated_icon_01_selected")),
NRExploreNovelMenuItem(type: .collected, title: "Most Collected".localized, icon: UIImage(named: "explore_most_collected_icon_01"), selectedIcon: UIImage(named: "explore_most_collected_icon_01_selected")),
NRExploreNovelMenuItem(type: .bestSellers, title: "Best Sellers".localized, icon: UIImage(named: "explore_best_sellers_icon_01"), selectedIcon: UIImage(named: "explore_best_sellers_icon_01_selected")),
]
return arr
}()
}

View File

@ -0,0 +1,188 @@
//
// NRHomeNovelListViewController.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import SnapKit
class NRHomeNovelListViewController: NRViewController {
lazy var dataArr: [NRNovelModel] = []
lazy var page = 1
lazy var collectionViewLayout: NRWaterfallFlowLayout = {
let layout = NRWaterfallFlowLayout()
layout.delegate = self
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.contentInset = .init(top: 0, left: 0, bottom:10, right: 0)
collectionView.nr_addRefreshFooter { [weak self] in
self?.handleFooterRefresh(nil)
}
collectionView.register(NRHomeNovelListCell.self, forCellWithReuseIdentifier: "cell")
collectionView.register(NRHomeNovelListTextCell.self, forCellWithReuseIdentifier: "textCell")
return collectionView
}()
lazy var listTitleView: NRHomeNovelHeaderContentView = {
let view = NRHomeNovelHeaderContentView()
view.titleLabel.text = "More Stories".localized
return view
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .clear
self.backgroundImageView.isHidden = true
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: NRNetworkReachableManager.networkStatusDidChangeNotification, object: nil)
nr_setupUI()
Task {
await requestDataArr(page: 1)
}
}
override func listScrollView() -> UIScrollView {
return collectionView
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: 1)
completer?()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: self.page + 1)
self.collectionView.nr_endFooterRefreshing()
completer?()
}
}
@objc private func networkStatusDidChangeNotification() {
if NRNetworkReachableManager.manager.isReachable == true, self.dataArr.isEmpty {
Task {
await requestDataArr(page: 1)
}
}
}
}
extension NRHomeNovelListViewController {
private func nr_setupUI() {
view.addSubview(listTitleView)
view.addSubview(collectionView)
listTitleView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
}
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
// make.top.equalTo(listTitleView.snp.bottom)
make.top.equalToSuperview().offset(36)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRHomeNovelListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = dataArr[indexPath.row]
let cellType = self.getCellType(indexPath: indexPath)
if cellType == 1 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "textCell", for: indexPath) as! NRHomeNovelListTextCell
cell.model = model
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRHomeNovelListCell
cell.model = model
return cell
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.didScrollCallback?(scrollView)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = NRNovelDetailViewController()
vc.novelId = self.dataArr[indexPath.row].id ?? ""
self.navigationController?.pushViewController(vc, animated: true)
}
func getCellType(indexPath: IndexPath) -> Int {
let num = indexPath.row + 1
if num % 3 == 2 {
return 1
}
return 0
}
}
//MARK: NRWaterfallMutiSectionDelegate
extension NRHomeNovelListViewController: NRWaterfallMutiSectionDelegate {
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: NRWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
let cellType = self.getCellType(indexPath: indexPath)
if cellType == 1 {
return NRHomeNovelListTextCell.contentHeight
} else {
let coverHeight = NRHomeNovelListCell.coverHeight
return coverHeight + 86
}
}
func lineSpacing(collectionView collection: UICollectionView, layout: NRWaterfallFlowLayout, section: Int) -> CGFloat {
return 16
}
func interitemSpacing(collectionView collection: UICollectionView, layout: NRWaterfallFlowLayout, section: Int) -> CGFloat {
return 13
}
func insetForSection(collectionView collection: UICollectionView, layout: NRWaterfallFlowLayout, section: Int) -> UIEdgeInsets {
return .init(top: 0, left: 15, bottom: 0, right: 15)
}
}
extension NRHomeNovelListViewController {
private func requestDataArr(page: Int) async {
guard let list = await NRHomeAPI.requestHomeNewData(page: page) else { return }
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.collectionView.reloadData()
}
}

View File

@ -0,0 +1,142 @@
//
// NRHomeNovelNewViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
import LYEmptyView
class NRHomeNovelNewViewController: NRViewController {
private lazy var dataArr: [NRNovelModel] = []
private lazy var page = 1
lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let itemWidth = (UIScreen.width - 32 - 40) / 3
let itemHeight = 150 / 100 * itemWidth + 68
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1 / 3), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(itemHeight)), subitems: [item])
group.interItemSpacing = .fixed(20)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 18
section.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
let configuration = UICollectionViewCompositionalLayoutConfiguration()
let layout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.ly_emptyView = NREmpty.nr_emptyView()
collectionView.contentInset = .init(top: 10, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
collectionView.nr_addRefreshHeader(insetTop: collectionView.contentInset.top) { [weak self] in
self?.handleHeaderRefresh(nil)
}
// collectionView.nr_addRefreshFooter(insetBottom: collectionView.contentInset.bottom) { [weak self] in
// self?.handleFooterRefresh(nil)
// }
collectionView.register(NRNovelGenresCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = .top
self.backgroundImageView.isHidden = true
self.title = "New".localized
configNavigationBack("arrow_left_icon_05")
nr_setupUI()
Task {
await requestDataArr(page: 1)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: 1)
self.collectionView.nr_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: self.page + 1)
self.collectionView.nr_endFooterRefreshing()
}
}
}
extension NRHomeNovelNewViewController {
private func nr_setupUI() {
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRHomeNovelNewViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRNovelGenresCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.row]
let vc = NRNovelDetailViewController()
vc.novelId = model.id ?? ""
self.navigationController?.pushViewController(vc, animated: true)
}
}
extension NRHomeNovelNewViewController {
private func requestDataArr(page: Int) async {
guard let list = await NRHomeAPI.requestNewData() else { return }
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.collectionView.reloadData()
}
}

View File

@ -0,0 +1,168 @@
//
// NRHomeNovelViewController.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import JXPagingView
import SnapKit
class NRHomeNovelViewController: NRViewController {
let viewModel = NRHomeNovelViewModel()
lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 16
view.layer.masksToBounds = true
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.F_2_EFEE.cgColor
return view
}()
lazy var headerView: NRHomeNovelHeaderView = {
let view = NRHomeNovelHeaderView()
view.heightDidChange = { [weak self] in
guard let self = self else { return }
self.pagerView.resizeTableHeaderViewHeight(animatable: false)
}
return view
}()
lazy var pagerView: JXPagingView = {
let view = JXPagingView(delegate: self)
view.layer.masksToBounds = true
view.mainTableView.backgroundColor = .clear
view.listContainerView.listCellBackgroundColor = .clear
view.mainTableView.gestureDelegate = self
view.mainTableView.nr_addRefreshHeader { [weak self] in
self?.handleHeaderRefresh(nil)
}
return view
}()
lazy var listVC: NRHomeNovelListViewController = {
let vc = NRHomeNovelListViewController()
return vc
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .clear
self.backgroundImageView.isHidden = true
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: NRNetworkReachableManager.networkStatusDidChangeNotification, object: nil)
Task {
await requestHomeData()
}
nr_setupUI()
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
self.listVC.handleHeaderRefresh(nil)
Task {
await requestHomeData()
self.pagerView.mainTableView.nr_endHeaderRefreshing()
}
}
@objc private func networkStatusDidChangeNotification() {
if NRNetworkReachableManager.manager.isReachable == true, self.viewModel.headerDataArr.isEmpty {
Task {
await requestHomeData()
}
}
}
}
extension NRHomeNovelViewController {
private func nr_setupUI() {
view.addSubview(bgView)
view.addSubview(pagerView)
bgView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(-1)
make.right.equalToSuperview().offset(1)
make.top.equalToSuperview().offset(10)
make.bottom.equalToSuperview().offset(UIScreen.tabBarHeight)
}
pagerView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(26)
}
}
}
//MARK: JXPagingViewDelegate
extension NRHomeNovelViewController: JXPagingViewDelegate {
func tableHeaderViewHeight(in pagingView: JXPagingView) -> Int {
return Int(ceil(self.headerView.contentHeight))
}
func tableHeaderView(in pagingView: JXPagingView) -> UIView {
return headerView
}
func heightForPinSectionHeader(in pagingView: JXPagingView) -> Int {
return 0
}
func viewForPinSectionHeader(in pagingView: JXPagingView) -> UIView {
return UIView()
}
func numberOfLists(in pagingView: JXPagingView) -> Int {
return 1
}
func pagingView(_ pagingView: JXPagingView, initListAtIndex index: Int) -> any JXPagingViewListViewDelegate {
return listVC
}
}
//MARK: JXPagingMainTableViewGestureDelegate
extension NRHomeNovelViewController: JXPagingMainTableViewGestureDelegate {
func mainTableViewGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// if otherGestureRecognizer == menuView.collectionView.panGestureRecognizer {
// return false
// }
if let view = otherGestureRecognizer.view {
var superview: UIView? = view.superview
while superview != nil {
if superview?.isKind(of: NRHomeNovelHeaderView.self) == true {
return false
}
superview = superview?.superview
}
}
return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
}
}
extension NRHomeNovelViewController {
private func requestHomeData() async {
await self.viewModel.requestHomeData()
self.headerView.dataArr = self.viewModel.headerDataArr
}
}

View File

@ -0,0 +1,68 @@
//
// NRHomeViewController.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import SnapKit
class NRHomeViewController: NRViewController {
lazy var searchButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "search_icon_01"), for: .normal)
button.addAction(UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let vc = NRSearchViewController()
self.navigationController?.pushViewController(vc, animated: true)
}), for: .touchUpInside)
return button
}()
lazy var titleView = UIImageView(image: UIImage(named: "home_title_image"))
lazy var novelVC = NRHomeNovelViewController()
override func viewDidLoad() {
super.viewDidLoad()
nr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
extension NRHomeViewController {
private func nr_setupUI() {
view.addSubview(searchButton)
view.addSubview(titleView)
addChild(novelVC)
view.addSubview(novelVC.view)
searchButton.snp.makeConstraints { make in
make.height.equalTo(44)
make.right.equalToSuperview().offset(-16)
make.top.equalTo(UIScreen.safeTop)
}
titleView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalTo(searchButton)
}
novelVC.view.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
}
}

View File

@ -0,0 +1,109 @@
//
// NRSearchViewController.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
class NRSearchViewController: NRViewController {
lazy var viewModel = NRSearchViewModel()
private lazy var returnButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "arrow_left_icon_02"), for: .normal)
button.addAction(UIAction(handler: { [weak self] _ in
self?.nr_handleNavigationBack()
}), for: .touchUpInside)
return button
}()
private lazy var textView: NRSearchInputView = {
let view = NRSearchInputView()
view.didSearch = { [weak self] text in
guard let self = self else { return }
self.search(text)
}
return view
}()
private lazy var homeView: NRSearchHomeView = {
let view = NRSearchHomeView()
view.viewModel = viewModel
view.didSearch = { [weak self] text in
self?.textView.text = text
self?.search(text)
}
return view
}()
private lazy var resultView: NRSearchResultView = {
let view = NRSearchResultView()
view.isHidden = true
view.viewModel = viewModel
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundImageView.isHidden = true
textView.becomeFirstResponder()
nr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
private func search(_ text: String) {
if text.isEmpty {
homeView.isHidden = false
resultView.isHidden = true
} else {
homeView.isHidden = true
resultView.isHidden = false
}
resultView.search(text)
self.viewModel.addSearchRecord(text: text)
}
}
extension NRSearchViewController {
private func nr_setupUI() {
view.addSubview(returnButton)
view.addSubview(textView)
view.addSubview(homeView)
view.addSubview(resultView)
returnButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview().offset(UIScreen.safeTop)
make.height.equalTo(44)
}
textView.snp.makeConstraints { make in
make.centerY.equalTo(returnButton)
make.left.equalTo(returnButton.snp.right).offset(12)
make.right.equalToSuperview().offset(-16)
}
homeView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(returnButton.snp.bottom)
}
resultView.snp.makeConstraints { make in
make.edges.equalTo(homeView)
}
}
}

View File

@ -0,0 +1,76 @@
//
// NRHomeNovelModuleItem.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/2.
//
import UIKit
import SmartCodable
class NRHomeNovelModuleItem: NSObject, SmartCodable {
enum ModuleKey: String, SmartCaseDefaultable {
case banner = "home_banner"
case v3_recommand = "home_v3_recommand"
///
case cagetory_recommand = "home_cagetory_recommand"
case week_ranking = "week_ranking"
///
case marquee = "marquee"
case new_recommand = "new_recommand"
case week_highest_recommend = "week_highest_recommend"
case get_details_recommand = "get_details_recommand"
case highest_payment_hot_video = "highest_payment_hot_video"
case category_navigation = "category_navigation"
}
required override init() { }
var module_key: ModuleKey?
var title: String?
var list: [NRNovelModel]?
var categoryList: [NRCategoryModel]?
@SmartAny
var data: Any?
@IgnoredKey
var iconImage: UIImage?
@IgnoredKey
var br_cellHeight: CGFloat?
func didFinishMapping() {
if module_key == .category_navigation, let data = data as? [[String : Any]] {
self.categoryList = [NRCategoryModel].deserialize(from: data)
} else if let data = data as? [[String : Any]] {
self.list = [NRNovelModel].deserialize(from: data)
} else if let data = data as? [String : Any] {
var dataList: [[String : Any]]?
if let list = data["list"] as? [[String : Any]] {
self.title = data["title"] as? String
dataList = list
} else if let list = data["shortPlayList"] as? [[String : Any]] {
self.title = data["category_name"] as? String
dataList = list
}
if let dataList = dataList {
self.list = [NRNovelModel].deserialize(from: dataList)
}
}
}
}

View File

@ -0,0 +1,20 @@
//
// NRReadWhatViewTransformer.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import FSPagerView
class NRReadWhatViewTransformer: FSPagerViewTransformer {
nonisolated override init(type: FSPagerViewTransformerType) {
super.init(type: type)
}
nonisolated override func proposedInteritemSpacing() -> CGFloat {
return -1
}
}

View File

@ -0,0 +1,53 @@
//
// NRHomeCategoryTagView.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
class NRHomeCategoryTagView: UIView {
override var intrinsicContentSize: CGSize {
return .init(width: 10, height: 20)
}
var text: String? {
set {
categoryLabel.text = newValue
}
get {
return categoryLabel.text
}
}
lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .black.withAlphaComponent(0.05)
layer.cornerRadius = 10
layer.masksToBounds = true
addSubview(categoryLabel)
categoryLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,69 @@
//
// NRHomeNovelHeaderContentView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
class NRHomeNovelHeaderContentView: UIView {
lazy var titleView: UIView = {
let view = UIView()
return view
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
return label
}()
lazy var indicatorImageView = UIImageView(image: UIImage(named: "arrow_right_icon_01"))
lazy var contentView: UIView = {
let view = UIView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
indicatorImageView.isHidden = true
addSubview(titleView)
titleView.addSubview(titleLabel)
titleView.addSubview(indicatorImageView)
addSubview(contentView)
titleView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.right.equalToSuperview().offset(-16)
make.top.equalToSuperview()
make.height.equalTo(24)
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview()
make.centerY.equalToSuperview()
}
indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(titleView.snp.bottom).offset(12)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,172 @@
//
// NRHomeNovelHeaderView.swift
// ReaderHive
//
// Created by on 2025/11/21.
//
import UIKit
import SnapKit
import YYCategories
class NRHomeNovelHeaderView: UIView {
var contentHeight: CGFloat {
return scrollView.contentSize.height + 1
}
var heightDidChange: (() -> Void)?
var dataArr: [NRHomeNovelModuleItem]? {
didSet {
self.stackView.nr_removeAllArrangedSubview()
dataArr?.forEach { item in
if item.module_key == .banner {
mustReadTodayView.dataArr = item.list ?? []
stackView.addArrangedSubview(mustReadTodayView)
} else if item.module_key == .new_recommand {
newArrivalsView.dataArr = item.list ?? []
stackView.addArrangedSubview(newArrivalsView)
} else if item.module_key == .get_details_recommand {
readWhatView.dataArr = item.list ?? []
stackView.addArrangedSubview(readWhatView)
} else if item.module_key == .highest_payment_hot_video {
hotGridView.dataArr = item.list ?? []
stackView.addArrangedSubview(hotGridView)
} else if item.module_key == .v3_recommand {
nextView.dataArr = item.list ?? []
stackView.addArrangedSubview(nextView)
} else if item.module_key == .category_navigation {
hotTagView.dataArr = item.categoryList ?? []
stackView.addArrangedSubview(hotTagView)
} else if item.module_key == .week_ranking {
featuredView.dataArr = item.list ?? []
stackView.addArrangedSubview(featuredView)
}
}
}
}
lazy var scrollView: NRScrollView = {
let scrollView = NRScrollView()
scrollView.isScrollEnabled = false
scrollView.addObserver(self, forKeyPath: "contentSize", context: nil)
return scrollView
}()
lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 24
return view
}()
lazy var mustReadTodayView: NRHomeNovelMustReadTodayView = {
let view = NRHomeNovelMustReadTodayView()
return view
}()
lazy var newArrivalsView: NRHomeNovelNewArrivalsView = {
let view = NRHomeNovelNewArrivalsView()
view.indicatorImageView.isHidden = false
view.titleView.addGestureRecognizer(UITapGestureRecognizer(actionBlock: { [weak self] _ in
guard let self = self else { return }
let vc = NRHomeNovelNewViewController()
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}))
return view
}()
lazy var readWhatView: NRHomeNovelReadWhatView = {
let view = NRHomeNovelReadWhatView()
return view
}()
lazy var hotGridView: NRHomeNovelHotGridView = {
let view = NRHomeNovelHotGridView()
return view
}()
lazy var nextView: NRHomeNovelNextView = {
let view = NRHomeNovelNextView()
return view
}()
lazy var hotTagView: NRHomeNovelHotTagView = {
let view = NRHomeNovelHotTagView()
return view
}()
lazy var featuredView: NRHomeNovelHotGridView = {
let view = NRHomeNovelHotGridView()
view.titleLabel.text = "Featured".localized
return view
}()
// lazy var listTitleView: NRHomeNovelHeaderContentView = {
// let view = NRHomeNovelHeaderContentView()
// view.titleLabel.text = "More Stories".localized
// return view
// }()
deinit {
scrollView.removeObserver(self, forKeyPath: "contentSize", context: nil)
}
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
self.heightDidChange?()
let height = scrollView.contentSize.height + 1
scrollView.snp.updateConstraints { make in
make.height.equalTo(height)
}
}
}
}
extension NRHomeNovelHeaderView {
private func nr_setupUI() {
addSubview(scrollView)
scrollView.addSubview(stackView)
scrollView.snp.makeConstraints { make in
make.left.right.top.bottom.equalToSuperview()
make.height.equalTo(1)
}
stackView.snp.makeConstraints { make in
make.left.centerX.equalToSuperview()
make.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-24)
}
}
}

View File

@ -0,0 +1,23 @@
//
// NRHomeNovelHotGridView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
class NRHomeNovelHotGridView: NRHomeNovelNewArrivalsView {
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.text = "Hot On The Grid".localized
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,37 @@
//
// NRHomeNovelHotTagCell.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
class NRHomeNovelHotTagCell: UICollectionViewCell {
lazy var textLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.backgroundColor = .black.withAlphaComponent(0.05)
self.contentView.layer.cornerRadius = 12
self.contentView.layer.masksToBounds = true
self.contentView.addSubview(textLabel)
textLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,140 @@
//
// NRHomeNovelHotTagView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
import collection_view_layouts
class NRHomeNovelHotTagView: UIView {
var dataArr: [NRCategoryModel] = [] {
didSet {
collectionView.reloadData()
}
}
lazy var bgView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "bg_image_03"))
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
label.text = "Hot Tags".localized
return label
}()
lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .F_9710_D
return label
}()
private lazy var collectionViewLayout: TagsLayout = {
let layout = TagsLayout()
layout.delegate = self
layout.contentPadding = ItemsPadding(horizontal: 16, vertical: 0)
layout.cellsPadding = ItemsPadding(horizontal: 12, vertical: 12)
return layout
}()
private lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
collectionView.register(NRHomeNovelHotTagCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
deinit {
collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .F_6_F_6_F_6
// subtitleLabel.text = "128 books updated yesterday"
nr_setupUI()
}
required init?(coder: NSCoder) {
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 NRHomeNovelHotTagView {
private func nr_setupUI() {
addSubview(bgView)
addSubview(titleLabel)
// addSubview(subtitleLabel)
addSubview(collectionView)
bgView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview().offset(12)
}
// subtitleLabel.snp.makeConstraints { make in
// make.left.equalToSuperview().offset(16)
// make.top.equalTo(titleLabel.snp.bottom).offset(5)
// }
collectionView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.top.equalTo(titleLabel.snp.bottom).offset(16)
make.bottom.equalToSuperview().offset(-16)
make.height.equalTo(1)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRHomeNovelHotTagView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRHomeNovelHotTagCell
cell.textLabel.text = dataArr[indexPath.row].name
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
}
extension NRHomeNovelHotTagView: LayoutDelegate {
func cellSize(indexPath: IndexPath) -> CGSize {
let text = dataArr[indexPath.row].name ?? ""
let size = text.size(.font(ofSize: 12, weight: .regular), .init(width: UIScreen.width, height: 24))
return .init(width: size.width + 24, height: 24)
}
}

View File

@ -0,0 +1,106 @@
//
// NRHomeNovelListCell.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
class NRHomeNovelListCell: UICollectionViewCell {
static let coverHeight: CGFloat = {
let width = (UIScreen.width - 32 - 13) / 2
let coverHeight = 247 / 165 * width
return coverHeight
}()
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
titleLabel.text = model?.name
desLabel.text = model?.nr_description
if let text = model?.category?.first, text.count > 0 {
self.categooryView.isHidden = false
self.categooryView.text = text
} else {
self.categooryView.isHidden = true
}
}
}
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.nr_setRoundedCorner(topLeft: 4, topRight: 4, bottomLeft: 0, bottomRight: 0)
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
label.numberOfLines = 2
return label
}()
lazy var categooryView: NRHomeCategoryTagView = {
let view = NRHomeCategoryTagView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.text = "Dungeons and Drama"
desLabel.text = "Struggling artist Lila signs a contract to marry the cold CEO Adrian Vance to save her family. Their \"business-only\" deal is shattered by electric tension and Adrian's jealous ex. As their facade crumbles under pressure, can their reluctant love survive the final, crushing truth?"
categooryView.text = "Satisfying"
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelListCell {
private func nr_setupUI() {
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(desLabel)
contentView.addSubview(categooryView)
coverImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(Self.coverHeight)
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalTo(coverImageView.snp.bottom).offset(8)
make.right.lessThanOrEqualToSuperview()
}
desLabel.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.right.lessThanOrEqualToSuperview()
}
categooryView.snp.makeConstraints { make in
make.bottom.equalToSuperview()
make.left.equalToSuperview()
}
}
}

View File

@ -0,0 +1,121 @@
//
// NRHomeNovelListTextCell.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
class NRHomeNovelListTextCell: UICollectionViewCell {
static let contentHeight = 218.0
var model: NRNovelModel? {
didSet {
textLabel.text = model?.nr_description
coverImageView.nr_setImage(model?.image_url)
nameLabel.text = model?.name
categoryLabel.text = model?.category?.first
}
}
lazy var textLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
label.numberOfLines = 0
return label
}()
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.15)
return view
}()
lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black
label.numberOfLines = 2
return label
}()
lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.25)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .F_2_EFEE
contentView.layer.cornerRadius = 8
contentView.layer.masksToBounds = true
textLabel.text = "The lamp in the embroidery room burned late into the night. Shen Qingci gripped the ruined piece of silk, the prick of the needle a familiar sting. She knew the sharpest weapon in the world was not a sword, but the human heart. Since marrying into the Marquis's estate, she felt trapped in a swamp. The seemingly docile concubine's sister, the always smiling-but-lethal mother-in-law—all were waiting for her downfall. She took a deep breath, pushing back the tears. From this day forward, she would not be a victim, but the one who wielded life and death within these walls."
nameLabel.text = "Fortunes Reborn-The 90s Reckoning"
categoryLabel.text = "Satisfying"
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelListTextCell {
private func nr_setupUI() {
contentView.addSubview(textLabel)
contentView.addSubview(coverImageView)
contentView.addSubview(lineView)
contentView.addSubview(nameLabel)
contentView.addSubview(categoryLabel)
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(8)
make.bottom.equalToSuperview().offset(-8)
make.width.equalTo(40)
make.height.equalTo(60)
}
lineView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(8)
make.centerX.equalToSuperview()
make.height.equalTo(1)
make.bottom.equalTo(coverImageView.snp.top).offset(-8)
}
textLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(8)
make.right.lessThanOrEqualToSuperview().offset(-8)
make.top.equalToSuperview().offset(8)
make.bottom.lessThanOrEqualTo(lineView.snp.top).offset(-8)
}
nameLabel.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(8)
make.right.lessThanOrEqualToSuperview().offset(-8)
make.top.equalTo(coverImageView).offset(0)
}
categoryLabel.snp.makeConstraints { make in
make.left.equalTo(nameLabel)
make.bottom.equalTo(coverImageView).offset(-7)
}
}
}

View File

@ -0,0 +1,179 @@
//
// NRHomeNovelMustReadTodayCell.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
class NRHomeNovelMustReadTodayCell: UICollectionViewCell {
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
titleLabel.text = model?.name
desLabel.text = model?.nr_description
starView.grade = model?.rate ?? 0
starView.text = NSNumber(value: model?.rate ?? 0).toString(maximumFractionDigits: 1, minimumFractionDigits: 1)
if let text = model?.category?.first, text.count > 0 {
categoryView.isHidden = false
categoryLabel.text = text
} else {
categoryView.isHidden = true
}
}
}
lazy var bgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "home_cell_bg_image_01"))
return view
}()
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
lazy var categoryView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.05)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
return view
}()
lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
label.numberOfLines = 2
return label
}()
lazy var readNowBgView: UIView = {
let view = NRGradientView()
view.colors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor]
view.startPoint = .init(x: 0, y: 0.5)
view.endPoint = .init(x: 1, y: 0.5)
view.layer.cornerRadius = 8
view.layer.masksToBounds = true
return view
}()
lazy var readNowLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .white
label.text = "Read Now".localized
return label
}()
lazy var starView: NRStarGradeView = {
let view = NRStarGradeView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.layer.cornerRadius = 12
self.contentView.layer.masksToBounds = true
categoryLabel.text = "Satisfying"
titleLabel.text = "A Strike to the Heart"
desLabel.text = "Haunted by fading memories, a man navigates a labyrinth of dreams and reality, uncovering truths that blur the line between past and present."
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelMustReadTodayCell {
private func nr_setupUI() {
contentView.addSubview(bgView)
contentView.addSubview(coverImageView)
contentView.addSubview(categoryView)
categoryView.addSubview(categoryLabel)
contentView.addSubview(titleLabel)
contentView.addSubview(starView)
contentView.addSubview(desLabel)
contentView.addSubview(readNowBgView)
readNowBgView.addSubview(readNowLabel)
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.centerY.equalToSuperview()
make.width.equalTo(100)
make.height.equalTo(150)
}
categoryView.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(12)
make.top.equalTo(coverImageView)
make.height.equalTo(20)
}
categoryLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(categoryView)
make.top.equalTo(coverImageView).offset(24)
make.right.lessThanOrEqualToSuperview().offset(-12)
}
starView.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(6)
}
desLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(33)
make.right.lessThanOrEqualToSuperview().offset(-12)
}
readNowBgView.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalTo(coverImageView)
make.right.equalToSuperview().offset(-12)
make.height.equalTo(36)
}
readNowLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
}

View File

@ -0,0 +1,86 @@
//
// NRHomeNovelMustReadTodayView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
import YYCategories
class NRHomeNovelMustReadTodayView: NRHomeNovelHeaderContentView {
var dataArr: [NRNovelModel] = [] {
didSet {
self.collectionView.reloadData()
}
}
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = .init(width: 316, height: 174)
layout.minimumLineSpacing = 16
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 16, bottom: 0, right: 16)
collectionView.register(NRHomeNovelMustReadTodayCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.titleLabel.text = "Must-Read Today".localized
nr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelMustReadTodayView {
private func nr_setupUI() {
contentView.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(self.collectionViewLayout.itemSize.height)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRHomeNovelMustReadTodayView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRHomeNovelMustReadTodayCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = NRNovelDetailViewController()
vc.novelId = dataArr[indexPath.row].id ?? ""
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,128 @@
//
// NRHomeNovelNewArrivalsCell.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
class NRHomeNovelNewArrivalsCell: UICollectionViewCell {
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
titleLabel.text = model?.name
if let text = model?.category?.first, text.count > 0 {
categoryView.isHidden = false
categoryLabel.text = text
} else if let text = model?.categoryList?.first?.name, text.count > 0 {
categoryView.isHidden = false
categoryLabel.text = text
} else {
categoryView.isHidden = true
}
if model?.tag_type == .new {
markImageView.image = UIImage(named: "new_icon_01")
markImageView.isHidden = false
} else if model?.tag_type == .hot {
markImageView.image = UIImage(named: "hot_icon_03")
markImageView.isHidden = false
} else {
markImageView.isHidden = true
}
}
}
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black
label.numberOfLines = 2
return label
}()
lazy var categoryView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.05)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
return view
}()
lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
lazy var markImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "new_icon_01"))
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelNewArrivalsCell {
private func nr_setupUI() {
contentView.addSubview(coverImageView)
coverImageView.addSubview(markImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(categoryView)
categoryView.addSubview(categoryLabel)
coverImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(135)
}
markImageView.snp.makeConstraints { make in
make.left.top.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
make.top.equalTo(coverImageView.snp.bottom).offset(4)
}
categoryView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.height.equalTo(20)
}
categoryLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
}
}

View File

@ -0,0 +1,84 @@
//
// NRHomeNovelNewArrivalsView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
import YYCategories
class NRHomeNovelNewArrivalsView: NRHomeNovelHeaderContentView {
var dataArr: [NRNovelModel] = [] {
didSet {
collectionView.reloadData()
}
}
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = .init(width: 90, height: 195)
layout.minimumLineSpacing = 12
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 16, bottom: 0, right: 16)
collectionView.register(NRHomeNovelNewArrivalsCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.titleLabel.text = "New Arrivals".localized
nr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelNewArrivalsView {
private func nr_setupUI() {
contentView.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.top.bottom.equalToSuperview()
make.height.equalTo(self.collectionViewLayout.itemSize.height)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRHomeNovelNewArrivalsView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRHomeNovelNewArrivalsCell
cell.model = dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = NRNovelDetailViewController()
vc.novelId = dataArr[indexPath.row].id ?? ""
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,89 @@
//
// NRHomeNovelNextView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
import YYCategories
class NRHomeNovelNextView: NRHomeNovelHeaderContentView {
var dataArr: [NRNovelModel] = [] {
didSet {
collectionView.reloadData()
}
}
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let itemWidth = (UIScreen.width - 32 - 30) / 3
let itemHeight = 150 / 100 * itemWidth + 68
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = .init(width: itemWidth, height: itemHeight)
layout.minimumLineSpacing = 15
layout.minimumInteritemSpacing = 18
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.isScrollEnabled = false
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 16, bottom: 0, right: 16)
collectionView.register(NRHomeNovelNextViewCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.titleLabel.text = "Next In View".localized
nr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelNextView {
private func nr_setupUI() {
contentView.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(collectionViewLayout.itemSize.height * 2 + collectionViewLayout.minimumInteritemSpacing)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRHomeNovelNextView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRHomeNovelNextViewCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return min(dataArr.count, 6)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = NRNovelDetailViewController()
vc.novelId = dataArr[indexPath.row].id ?? ""
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,128 @@
//
// NRHomeNovelNextViewCell.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
class NRHomeNovelNextViewCell: UICollectionViewCell {
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
titleLabel.text = model?.name
if let text = model?.category?.first, text.count > 0 {
categoryView.isHidden = false
categoryLabel.text = text
} else if let text = model?.categoryList?.first?.name, text.count > 0 {
categoryView.isHidden = false
categoryLabel.text = text
} else {
categoryView.isHidden = true
}
if model?.tag_type == .new {
markImageView.image = UIImage(named: "new_icon_01")
markImageView.isHidden = false
} else if model?.tag_type == .hot {
markImageView.image = UIImage(named: "hot_icon_03")
markImageView.isHidden = false
} else {
markImageView.isHidden = true
}
}
}
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
label.numberOfLines = 2
return label
}()
lazy var categoryView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.05)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
return view
}()
lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
lazy var markImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "new_icon_01"))
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
categoryLabel.text = "Satisfying"
titleLabel.text = "Vanished Without a Word"
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelNextViewCell {
private func nr_setupUI() {
contentView.addSubview(coverImageView)
coverImageView.addSubview(markImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(categoryView)
categoryView.addSubview(categoryLabel)
coverImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-68)
}
markImageView.snp.makeConstraints { make in
make.left.top.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
make.top.equalTo(coverImageView.snp.bottom).offset(8)
}
categoryView.snp.makeConstraints { make in
make.left.equalToSuperview()
// make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.bottom.equalToSuperview()
make.height.equalTo(20)
}
categoryLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
}
}

View File

@ -0,0 +1,50 @@
//
// NRHomeNovelReadWhatCell.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import FSPagerView
import SnapKit
class NRHomeNovelReadWhatCell: FSPagerViewCell {
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
}
}
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.layer.shadowColor = UIColor.clear.cgColor
nr_setupUI()
}
@MainActor required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRHomeNovelReadWhatCell {
private func nr_setupUI() {
contentView.addSubview(coverImageView)
coverImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}

View File

@ -0,0 +1,301 @@
//
// NRHomeNovelReadWhatView.swift
// ReaderHive
//
// Created by on 2025/11/24.
//
import UIKit
import SnapKit
import FSPagerView
import YYCategories
class NRHomeNovelReadWhatView: UIView {
var dataArr: [NRNovelModel] = [] {
didSet {
pageView.reloadData()
updateCurrentNodelData()
}
}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
label.text = "read_what_title".localized
return label
}()
lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .F_9710_D
label.text = "read_what_subtitle".localized
return label
}()
lazy var bgView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "bg_image_02"))
imageView.isUserInteractionEnabled = true
return imageView
}()
lazy var pageView: FSPagerView = {
let transformer = NRReadWhatViewTransformer(type: .linear)
transformer.minimumScale = 0.7
let view = FSPagerView()
view.layer.masksToBounds = false
view.transformer = transformer
view.delegate = self
view.dataSource = self
view.isInfinite = true
view.itemSize = .init(width: 120, height: 180)
view.register(NRHomeNovelReadWhatCell.self, forCellWithReuseIdentifier: "cell")
return view
}()
lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
label.numberOfLines = 4
return label
}()
lazy var readNowButton: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.image = UIImage(named: "arrow_right_icon_02")
configuration.imagePlacement = .trailing
configuration.imagePadding = 4
configuration.attributedTitle = AttributedString("Read Now".localized, attributes: AttributeContainer([
.font : UIFont.font(ofSize: 14, weight: .medium),
.foregroundColor : UIColor.black
]))
configuration.contentInsets = .init(top: 0, leading: 13, bottom: 0, trailing: 13)
let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let vc = NRNovelDetailViewController()
vc.novelId = self.dataArr[self.pageView.currentIndex].id ?? ""
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}))
button.layer.cornerRadius = 12
button.layer.masksToBounds = true
button.layer.borderColor = UIColor.black.withAlphaComponent(0.25).cgColor
button.layer.borderWidth = 1
return button
}()
lazy var tagStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 5
return stackView
}()
lazy var categoryView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.05)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
return view
}()
lazy var categoryLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
lazy var hotView: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.image = UIImage(named: "hot_icon_01")
configuration.background.backgroundColor = .black.withAlphaComponent(0.05)
configuration.contentInsets = .init(top: 0, leading: 8, bottom: 0, trailing: 8)
configuration.imagePadding = 0
let button = UIButton(configuration: configuration)
button.layer.cornerRadius = 10
button.layer.masksToBounds = true
button.isUserInteractionEnabled = false
button.configurationUpdateHandler = { [weak self] button in
guard let self = self else { return }
let index = self.pageView.currentIndex
if index >= self.dataArr.count { return }
let model = self.dataArr[index]
button.configuration?.attributedTitle = AttributedString(NSNumber(value: model.watch_total ?? 0).formattedNumber(), attributes: AttributeContainer([
.font : UIFont.font(ofSize: 10, weight: .regular),
.foregroundColor : UIColor.black.withAlphaComponent(0.5)
]))
}
return button
}()
lazy var starView: NRStarGradeView = {
let view = NRStarGradeView()
view.grade = 0
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
// tagStackView.addArrangedSubview(categoryView)
// tagStackView.addArrangedSubview(hotView)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateCurrentNodelData() {
let index = self.pageView.currentIndex
nrPrint(message: index)
let model = dataArr[index]
nameLabel.text = model.name
desLabel.text = model.nr_description
starView.grade = model.rate ?? 0
starView.text = NSNumber(value: model.rate ?? 0).toString(maximumFractionDigits: 1, minimumFractionDigits: 1)
tagStackView.nr_removeAllArrangedSubview()
if let text = model.category?.first, text.count > 0 {
categoryLabel.text = text
tagStackView.addArrangedSubview(categoryView)
}
tagStackView.addArrangedSubview(hotView)
hotView.setNeedsUpdateConfiguration()
}
}
extension NRHomeNovelReadWhatView {
private func nr_setupUI() {
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(bgView)
bgView.addSubview(pageView)
bgView.addSubview(nameLabel)
bgView.addSubview(tagStackView)
bgView.addSubview(desLabel)
bgView.addSubview(readNowButton)
bgView.addSubview(starView)
categoryView.addSubview(categoryLabel)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview().offset(12)
}
subtitleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalTo(titleLabel.snp.bottom).offset(5)
}
bgView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.centerX.equalToSuperview()
make.top.equalTo(subtitleLabel.snp.bottom).offset(16)
make.height.equalTo(385)
make.bottom.equalToSuperview()
}
pageView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(12)
make.height.equalTo(180)
}
nameLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(-16)
make.top.equalTo(pageView.snp.bottom).offset(12)
}
tagStackView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.height.equalTo(20)
make.top.equalTo(nameLabel.snp.bottom).offset(8)
}
desLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(-16)
make.top.equalTo(nameLabel.snp.bottom).offset(36)
}
readNowButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-16)
make.height.equalTo(24)
// make.top.equalTo(desLabel.snp.bottom).offset(20)
make.bottom.equalToSuperview().offset(-12)
}
starView.snp.makeConstraints { make in
make.centerY.equalTo(readNowButton)
make.left.equalToSuperview().offset(16)
// make.width.equalTo(90)
// make.height.equalTo(16)
}
categoryLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
}
}
//MARK: FSPagerViewDelegate FSPagerViewDataSource
extension NRHomeNovelReadWhatView: FSPagerViewDelegate, FSPagerViewDataSource {
func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell {
let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "cell", at: index) as! NRHomeNovelReadWhatCell
cell.model = dataArr[index]
return cell
}
func numberOfItems(in pagerView: FSPagerView) -> Int {
return dataArr.count
}
func pagerViewDidEndScrollAnimation(_ pagerView: FSPagerView) {
nrPrint(message: "pagerViewDidEndScrollAnimation")
}
func pagerViewDidEndDecelerating(_ pagerView: FSPagerView) {
nrPrint(message: "pagerViewDidEndDecelerating")
updateCurrentNodelData()
}
func pagerView(_ pagerView: FSPagerView, didSelectItemAt index: Int) {
let vc = NRNovelDetailViewController()
vc.novelId = dataArr[index].id ?? ""
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,88 @@
//
// NRSearchHomeView.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
class NRSearchHomeView: UIView {
weak var viewModel: NRSearchViewModel? {
didSet {
// viewModel?.addObserver(self, forKeyPath: "recommendData", context: nil)
viewModel?.addObserver(self, forKeyPath: "recordList", context: nil)
self.recordView.dataArr = self.viewModel?.recordList ?? []
updateLayout()
}
}
var didSearch: ((_ text: String) -> Void)?
private lazy var stackView: UIStackView = {
let view = UIStackView()
view.spacing = 20
view.axis = .vertical
return view
}()
private lazy var recordView: NRSearchRecordView = {
let view = NRSearchRecordView()
view.didSearch = { [weak self] text in
self?.didSearch?(text)
}
view.didDelete = { [weak self] in
self?.viewModel?.clearSearchRecord()
}
return view
}()
deinit {
self.viewModel?.removeObserver(self, forKeyPath: "recordList")
}
override init(frame: CGRect) {
super.init(frame: frame)
updateLayout()
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "recordList" {
self.recordView.dataArr = self.viewModel?.recordList ?? []
}
updateLayout()
}
func updateLayout() {
stackView.nr_removeAllArrangedSubview()
if self.recordView.dataArr.count > 0 {
stackView.addArrangedSubview(recordView)
}
}
}
extension NRSearchHomeView {
private func nr_setupUI() {
addSubview(stackView)
stackView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(26)
make.bottom.lessThanOrEqualToSuperview()
}
}
}

View File

@ -0,0 +1,109 @@
//
// NRSearchInputView.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
class NRSearchInputView: UIView {
override var intrinsicContentSize: CGSize {
return .init(width: UIScreen.width, height: 36)
}
var didSearch: ((_ text: String) -> Void)?
var text: String? {
get {
return textField.text
}
set {
textField.text = newValue
}
}
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "search_icon_02"))
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
return imageView
}()
private lazy var textField: UITextField = {
let textField = UITextField(frame: .zero)
textField.tintColor = UIColor.black.withAlphaComponent(0.5)
textField.delegate = self
textField.returnKeyType = .search
textField.font = .font(ofSize: 12, weight: .regular)
textField.textColor = .black.withAlphaComponent(0.5)
textField.attributedPlaceholder = NSAttributedString(string: "search_placeholder".localized, attributes: [
.font : UIFont.font(ofSize: 12, weight: .regular),
.foregroundColor : UIColor.black.withAlphaComponent(0.15)
])
return textField
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 18
self.layer.masksToBounds = true
self.backgroundColor = .F_2_EFEE
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@discardableResult
override func becomeFirstResponder() -> Bool {
super.becomeFirstResponder()
return self.textField.becomeFirstResponder()
}
@discardableResult
override func resignFirstResponder() -> Bool {
super.resignFirstResponder()
return self.textField.resignFirstResponder()
}
}
extension NRSearchInputView {
private func nr_setupUI() {
addSubview(iconImageView)
addSubview(textField)
iconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(12)
}
textField.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.left.equalTo(iconImageView.snp.right).offset(10)
make.right.equalToSuperview().offset(-15)
}
}
}
//MARK: UITextFieldDelegate
extension NRSearchInputView: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
if let text = textField.text {
self.didSearch?(text)
}
return true
}
}

View File

@ -0,0 +1,40 @@
//
// NRSearchRecordCell.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
class NRSearchRecordCell: UICollectionViewCell {
static let TextFont: UIFont = .font(ofSize: 10, weight: .regular)
lazy var textLabel: UILabel = {
let label = UILabel()
label.font = Self.TextFont
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.layer.cornerRadius = 10
contentView.layer.masksToBounds = true
contentView.backgroundColor = .black.withAlphaComponent(0.05)
contentView.addSubview(textLabel)
textLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,136 @@
//
// NRSearchRecordView.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import collection_view_layouts
import SnapKit
import YYCategories
class NRSearchRecordView: UIView {
var didSearch: ((_ text: String) -> Void)?
var didDelete: (() -> Void)?
var dataArr: [String] = [] {
didSet {
self.collectionView.reloadData()
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
label.text = "Recent Searches".localized
return label
}()
private lazy var deleteButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "delete_icon_01"), for: .normal)
button.addAction(UIAction(handler: { [weak self] _ in
self?.didDelete?()
}), for: .touchUpInside)
return button
}()
private lazy var collectionViewLayout: TagsLayout = {
let layout = TagsLayout()
layout.delegate = self
layout.contentPadding = ItemsPadding(horizontal: 16, vertical: 0)
layout.cellsPadding = ItemsPadding(horizontal: 12, vertical: 12)
return layout
}()
private lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
collectionView.register(NRSearchRecordCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
deinit {
self.collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) {
super.init(frame: frame)
fa_setupLayout()
}
required init?(coder: NSCoder) {
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
self.collectionView.snp.updateConstraints { make in
make.height.equalTo(height + 1)
}
}
}
}
extension NRSearchRecordView {
private func fa_setupLayout() {
addSubview(titleLabel)
addSubview(deleteButton)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.centerY.equalTo(deleteButton)
}
deleteButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-16)
make.top.equalToSuperview()
}
collectionView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(36)
make.left.equalToSuperview()
make.right.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(1)
}
}
}
//MARK: UICollectionViewDataSource UICollectionViewDataSource
extension NRSearchRecordView: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRSearchRecordCell
cell.textLabel.text = dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.didSearch?(dataArr[indexPath.row])
}
}
//MARK: LayoutDelegate
extension NRSearchRecordView: LayoutDelegate {
func cellSize(indexPath: IndexPath) -> CGSize {
let text = dataArr[indexPath.row]
let size = text.size(NRSearchRecordCell.TextFont, .init(width: UIScreen.width, height: 24))
return .init(width: size.width + 16, height: 20)
}
}

View File

@ -0,0 +1,137 @@
//
// NRSearchResultCell.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
class NRSearchResultCell: UICollectionViewCell {
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
desLabel.text = model?.name
tagStackView.nr_removeAllArrangedSubview()
if let text = model?.categoryList?.first?.name, text.count > 0 {
categoryView.text = text
tagStackView.addArrangedSubview(categoryView)
}
tagStackView.addArrangedSubview(hotView)
hotView.setNeedsUpdateConfiguration()
}
}
lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
return imageView
}()
lazy var tagStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 8
return stackView
}()
lazy var categoryView: NRHomeCategoryTagView = {
let view = NRHomeCategoryTagView()
return view
}()
lazy var hotView: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.image = UIImage(named: "hot_icon_01")
configuration.background.backgroundColor = .black.withAlphaComponent(0.05)
configuration.contentInsets = .init(top: 0, leading: 8, bottom: 0, trailing: 8)
configuration.imagePadding = 0
let button = UIButton(configuration: configuration)
button.layer.cornerRadius = 10
button.layer.masksToBounds = true
button.isUserInteractionEnabled = false
button.configurationUpdateHandler = { [weak self] button in
guard let self = self else { return }
let text = NSNumber(value: model?.watch_total ?? 0).formattedNumber()
button.configuration?.attributedTitle = AttributedString(text, attributes: AttributeContainer([
.font : UIFont.font(ofSize: 10, weight: .regular),
.foregroundColor : UIColor.black.withAlphaComponent(0.5)
]))
}
return button
}()
lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
label.numberOfLines = 2
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
categoryView.text = "Satisfying"
nameLabel.text = "My Dark Romeo"
desLabel.text = "Haunted by fading memories, a man navigates a labyrinth of dreams and reality, uncovering truths that blur the line between past and present."
tagStackView.addArrangedSubview(categoryView)
tagStackView.addArrangedSubview(hotView)
// categoryView.addSubview(hotView)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRSearchResultCell {
private func nr_setupUI() {
contentView.addSubview(coverImageView)
contentView.addSubview(tagStackView)
contentView.addSubview(nameLabel)
contentView.addSubview(desLabel)
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.bottom.equalToSuperview()
make.width.equalTo(60)
}
tagStackView.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(12)
make.bottom.equalToSuperview()
make.height.equalTo(20)
}
nameLabel.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(12)
make.right.lessThanOrEqualToSuperview().offset(-16)
make.top.equalToSuperview()
}
desLabel.snp.makeConstraints { make in
make.left.equalTo(nameLabel)
make.right.lessThanOrEqualToSuperview().offset(-16)
make.top.equalTo(nameLabel.snp.bottom).offset(7)
}
}
}

View File

@ -0,0 +1,106 @@
//
// NRSearchResultView.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
import SnapKit
import LYEmptyView
import YYCategories
class NRSearchResultView: UIView {
weak var viewModel: NRSearchViewModel?
var searchText: String = ""
lazy var dataArr: [NRNovelModel] = []
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = .init(width: UIScreen.width, height: 90)
layout.minimumLineSpacing = 16
layout.minimumInteritemSpacing = 16
return layout
}()
lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.contentInset = .init(top: 10, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
collectionView.ly_emptyView = NREmpty.nr_emptyView()
collectionView.register(NRSearchResultCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func search(_ text: String) {
if text.isEmpty {
self.dataArr.removeAll()
self.collectionView.reloadData()
return
}
self.searchText = text
Task {
await requestDataArr(text: self.searchText)
}
}
}
extension NRSearchResultView {
private func nr_setupUI() {
addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.bottom.right.equalToSuperview()
make.top.equalToSuperview().offset(10)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRSearchResultView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRSearchResultCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = self.dataArr[indexPath.row]
let vc = NRNovelDetailViewController()
vc.novelId = model.id ?? ""
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}
extension NRSearchResultView {
private func requestDataArr(text: String) async {
guard let list = await NRHomeAPI.requestSearchNovel(text: text) else { return }
guard text == self.searchText else { return }
self.dataArr = list
self.collectionView.reloadData()
}
}

View File

@ -0,0 +1,118 @@
//
// NRStarGradeView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/2.
//
import UIKit
import SnapKit
import Cosmos
class NRStarGradeView: UIView {
//0-10
var grade: CGFloat {
set {
cosmosView.rating = newValue
}
get {
return cosmosView.rating
}
}
var text: String? {
set {
cosmosView.text = newValue
}
get {
return cosmosView.text
}
}
var filledImage: UIImage? = UIImage(named: "star_icon_02") {
didSet {
settings.filledImage = filledImage
settings.starSize = Double(settings.filledImage?.size.width ?? 0)
cosmosView.settings = settings
}
}
var emptyImage: UIImage? = UIImage(named: "star_icon_03") {
didSet {
settings.emptyImage = emptyImage
cosmosView.settings = settings
}
}
///
var updateOnTouch: Bool = true {
didSet {
// settings.updateOnTouch = updateOnTouch
// cosmosView.settings = settings
self.isUserInteractionEnabled = updateOnTouch
}
}
var fillMode: StarFillMode = .precise {
didSet {
settings.fillMode = fillMode
cosmosView.settings = settings
}
}
var didTouch: ((Double)->())?
var didFinishTouching: ((Double)->())?
private lazy var settings: CosmosSettings = {
var settings = CosmosSettings.default
settings.filledImage = filledImage
settings.emptyImage = emptyImage
settings.starSize = Double(settings.filledImage?.size.width ?? 0)
settings.starMargin = 4
settings.fillMode = fillMode
settings.textFont = .font(ofSize: 12, weight: .regular)
settings.textColor = .black
settings.textMargin = 4
settings.minTouchRating = 1
settings.updateOnTouch = true
return settings
}()
private lazy var cosmosView: CosmosView = {
let view = CosmosView(settings: settings)
view.didTouchCosmos = { [weak self] rating in
self?.didTouch?(rating)
}
view.didFinishTouchingCosmos = { [weak self] rating in
self?.didFinishTouching?(rating)
}
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
nr_setupUI()
}
}
extension NRStarGradeView {
private func nr_setupUI() {
addSubview(cosmosView)
cosmosView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}

View File

@ -0,0 +1,74 @@
//
// NRHomeNovelViewModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/2.
//
import UIKit
class NRHomeNovelViewModel: NSObject {
lazy var headerDataArr: [NRHomeNovelModuleItem] = []
func requestHomeData() async {
guard let list = await NRHomeAPI.requestHomeData() else { return }
var item1: NRHomeNovelModuleItem?
var item2: NRHomeNovelModuleItem?
var item3: NRHomeNovelModuleItem?
var item4: NRHomeNovelModuleItem?
var item5: NRHomeNovelModuleItem?
var item6: NRHomeNovelModuleItem?
var item7: NRHomeNovelModuleItem?
list.forEach { item in
if item.module_key == .banner, let list = item.list, list.count > 0 {
item1 = item
} else if item.module_key == .new_recommand, let list = item.list, list.count > 0 {
item2 = item
} else if item.module_key == .get_details_recommand, let list = item.list, list.count > 0 {
item3 = item
} else if item.module_key == .highest_payment_hot_video, let list = item.list, list.count > 0 {
item4 = item
} else if item .module_key == .v3_recommand, let list = item.list, list.count > 0 {
item5 = item
} else if item .module_key == .category_navigation, let list = item.categoryList, list.count > 0 {
item6 = item
} else if item .module_key == .week_ranking, let list = item.list, list.count > 0 {
item7 = item
}
}
headerDataArr.removeAll()
if let item = item1 {
headerDataArr.append(item)
}
if let item = item2 {
headerDataArr.append(item)
}
if let item = item3 {
headerDataArr.append(item)
}
if let item = item4 {
headerDataArr.append(item)
}
if let item = item5 {
headerDataArr.append(item)
}
if let item = item6 {
headerDataArr.append(item)
}
if let item = item7 {
headerDataArr.append(item)
}
}
}

View File

@ -0,0 +1,45 @@
//
// NRSearchViewModel.swift
// ReaderHive
//
// Created by on 2025/11/25.
//
import UIKit
class NRSearchViewModel: NSObject {
static let searchRecordUserDefaultKey = "NRSearchViewModel.searchRecordUserDefaultKey"
@objc dynamic private(set) var recordList: [String] = (UserDefaults.standard.object(forKey: NRSearchViewModel.searchRecordUserDefaultKey) as? [String]) ?? []
func addSearchRecord(text: String) {
guard !text.isEmpty else { return }
var list = recordList
for (index, value) in list.enumerated() {
if value == text {
list.remove(at: index)
break
}
}
list.insert(text, at: 0)
if list.count > 10 {
list.removeLast()
}
recordList = list
UserDefaults.standard.set(list, forKey: NRSearchViewModel.searchRecordUserDefaultKey)
}
func clearSearchRecord() {
recordList.removeAll()
UserDefaults.standard.set(recordList, forKey: NRSearchViewModel.searchRecordUserDefaultKey)
}
}

View File

@ -0,0 +1,21 @@
//
// NRLanguageModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/8.
//
import UIKit
import SmartCodable
class NRLanguageModel: NSObject, SmartCodable {
required override init() { }
var id: String?
var cn_name: String?
var show_name: String?
var lang_key: String?
var is_up_to_list: String?
}

View File

@ -0,0 +1,25 @@
//
// NRMeItem.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
struct NRMeItem {
enum ItemType {
case history
case language
case about
case settings
case web
}
var type: ItemType
var icon: UIImage?
var title: String
var url: String?
}

View File

@ -0,0 +1,49 @@
//
// NRAboutCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
class NRAboutCell: NRTableViewCell {
var item: NRMeItem? {
didSet {
titleLabel.text = item?.title
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
nr_indicatorImageView.image = UIImage(named: "arrow_right_icon_06")
contentView.addSubview(titleLabel)
contentView.addSubview(nr_indicatorImageView)
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(16)
}
nr_indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-16)
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,82 @@
//
// NRAboutHeaderView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
class NRAboutHeaderView: UIView {
private lazy var appLogoView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "logo_icon_01"))
imageView.layer.cornerRadius = 8
imageView.layer.masksToBounds = true
imageView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(handleLogoImageView))
imageView.addGestureRecognizer(tap)
return imageView
}()
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
label.text = kNRAPPName
return label
}()
private lazy var versionLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = ._999999
label.text = "Version \(kNRAPPVersion)"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func handleLogoImageView() {
guard let url = URL(string: NRWebBaseURL) else { return }
UIApplication.shared.open(url)
}
}
extension NRAboutHeaderView {
private func nr_setupUI() {
addSubview(appLogoView)
addSubview(nameLabel)
addSubview(versionLabel)
appLogoView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(24)
make.width.height.equalTo(72)
}
nameLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(appLogoView.snp.bottom).offset(12)
}
versionLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(nameLabel.snp.bottom).offset(5)
}
}
}

View File

@ -0,0 +1,76 @@
//
// NRLanguageCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/8.
//
import UIKit
import SnapKit
class NRLanguageCell: UICollectionViewCell {
var model: NRLanguageModel? {
didSet {
titleLabel.text = model?.show_name
}
}
var nr_isSelected: Bool = false {
didSet {
if nr_isSelected {
contentView.layer.borderColor = UIColor.F_9710_D.cgColor
selectedImageView.image = UIImage(named: "checked_icon_01_selected")
} else {
contentView.layer.borderColor = UIColor.clear.cgColor
selectedImageView.image = UIImage(named: "checked_icon_01")
}
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
private lazy var selectedImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .F_2_EFEE
contentView.layer.cornerRadius = 8
contentView.layer.masksToBounds = true
contentView.layer.borderWidth = 1
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRLanguageCell {
private func nr_setupUI() {
contentView.addSubview(titleLabel)
contentView.addSubview(selectedImageView)
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(12)
}
selectedImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-12)
}
}
}

View File

@ -0,0 +1,68 @@
//
// NRMeCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
class NRMeCell: NRTableViewCell {
var item: NRMeItem? {
didSet {
iconImageView.image = item?.icon
titleLabel.text = item?.title
}
}
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.contentView.backgroundColor = .white
nr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRMeCell {
private func nr_setupUI() {
contentView.addSubview(iconImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(nr_indicatorImageView)
iconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(12)
}
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(60)
}
nr_indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-12)
}
}
}

View File

@ -0,0 +1,103 @@
//
// NRMeCoinsContentView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/8.
//
import UIKit
import SnapKit
class NRMeCoinsContentView: UIView {
override var intrinsicContentSize: CGSize {
return .init(width: UIScreen.width, height: 60)
}
var userInfo: NRUserInfo? {
didSet {
coinsView.count = userInfo?.coin_left_total
sendCoinsView.count = userInfo?.send_coin_left_total
}
}
private lazy var coinsView: NRMeCoinsView = {
let view = NRMeCoinsView()
view.title = "Coins".localized
return view
}()
private lazy var sendCoinsView: NRMeCoinsView = {
let view = NRMeCoinsView()
view.title = "Bonus".localized
return view
}()
private lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = .F_2_EFEE
return view
}()
private lazy var topUpButton: UIButton = {
let button = NRGradientButton(type: .custom)
button.colors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor]
button.startPoint = .init(x: 0, y: 0.5)
button.endPoint = .init(x: 1, y: 0.5)
button.layer.cornerRadius = 18
button.layer.masksToBounds = true
button.setTitle("Top Up".localized, for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.titleLabel?.font = .font(ofSize: 14, weight: .medium)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = 8
layer.masksToBounds = true
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRMeCoinsContentView {
private func nr_setupUI() {
addSubview(coinsView)
addSubview(sendCoinsView)
addSubview(lineView)
addSubview(topUpButton)
coinsView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.centerY.equalToSuperview()
}
sendCoinsView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(lineView.snp.right).offset(20)
}
lineView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(coinsView.snp.right).offset(20)
make.width.equalTo(1)
make.height.equalTo(24)
}
topUpButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-12)
make.width.equalTo(100)
make.height.equalTo(36)
}
}
}

View File

@ -0,0 +1,76 @@
//
// NRMeCoinsView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/8.
//
import UIKit
import SnapKit
class NRMeCoinsView: UIView {
var title: String? {
didSet {
titleLabel.text = title
}
}
var count: Int? {
didSet {
coinLabel.text = "\(count ?? 0)"
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .semibold)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "coins_icon_01"))
return imageView
}()
private lazy var coinLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 18, weight: .regular)
label.textColor = .F_9710_D
label.text = "0"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(titleLabel)
addSubview(iconImageView)
addSubview(coinLabel)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
}
iconImageView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.bottom.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(5)
}
coinLabel.snp.makeConstraints { make in
make.left.equalTo(iconImageView.snp.right).offset(4)
make.centerY.equalTo(iconImageView)
make.right.lessThanOrEqualToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,146 @@
//
// NRMeHeaderView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
class NRMeHeaderView: UITableViewHeaderFooterView {
// var contentHeight: CGFloat = 200
private lazy var avatarImageView: UIImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 30
imageView.layer.borderWidth = 1
imageView.layer.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor
return imageView
}()
private lazy var nickLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 18, weight: .semibold)
label.textColor = .black
return label
}()
private lazy var idBgView: UIView = {
let view = UIView()
view.backgroundColor = .F_2_EFEE
view.layer.cornerRadius = 9
view.layer.masksToBounds = true
return view
}()
private lazy var idLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .black
return label
}()
private lazy var copyButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
UIPasteboard.general.string = NRLoginManager.manager.userInfo?.customer_id
NRToast.show(text: "Success")
}))
button.setImage(UIImage(named: "copy_icon_01"), for: .normal)
return button
}()
private lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 16
return view
}()
private lazy var coinsView: NRMeCoinsContentView = {
let view = NRMeCoinsContentView()
return view
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: NRLoginManager.userInfoUpdateNotification, object: nil)
nr_setupUI()
userInfoUpdateNotification()
stackView.addArrangedSubview(coinsView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func userInfoUpdateNotification() {
guard let userInfo = NRLoginManager.manager.userInfo else { return }
avatarImageView.nr_setImage(userInfo.avator)
nickLabel.text = userInfo.family_name?.isEmpty != false ? "Visitor".localized : userInfo.family_name
idLabel.text = "ID\(userInfo.customer_id ?? "")"
coinsView.userInfo = userInfo
}
}
extension NRMeHeaderView {
private func nr_setupUI() {
contentView.addSubview(avatarImageView)
contentView.addSubview(nickLabel)
contentView.addSubview(idBgView)
idBgView.addSubview(idLabel)
idBgView.addSubview(copyButton)
// contentView.addSubview(stackView)
avatarImageView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalToSuperview().offset(48)
make.width.height.equalTo(60)
make.bottom.equalToSuperview().offset(-16)
}
nickLabel.snp.makeConstraints { make in
make.left.equalTo(avatarImageView.snp.right).offset(16)
make.top.equalTo(avatarImageView).offset(5)
}
idBgView.snp.makeConstraints { make in
make.left.equalTo(nickLabel)
make.bottom.equalTo(avatarImageView).offset(-5)
make.height.equalTo(18)
}
idLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
copyButton.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.right.equalToSuperview().offset(-8)
make.left.equalTo(idLabel.snp.right).offset(8)
}
// stackView.snp.makeConstraints { make in
// make.left.right.equalToSuperview()
// make.top.equalTo(avatarImageView.snp.bottom).offset(16)
// make.bottom.equalToSuperview().offset(-16)
// }
}
}

View File

@ -0,0 +1,134 @@
//
// NRNovelHistoryCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
class NRNovelHistoryCell: UICollectionViewCell {
var model: NRNovelModel? {
didSet {
coverImageView.nr_setImage(model?.image_url)
titleLabel.text = model?.name
chLabel.text = "Ch.##".localizedReplace(text: "\(model?.current_episode ?? 0)") + " / " + "Ch.##".localizedReplace(text: "\(model?.episode_total ?? 0)")
if let text = model?.category?.first, text.count > 0 {
categoryView.text = text
categoryView.isHidden = false
} else {
categoryView.isHidden = true
}
collectButton.isSelected = model?.is_collect ?? false
}
}
private lazy var coverImageView: NRImageView = {
let imageView = NRImageView()
imageView.layer.cornerRadius = 4
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
private lazy var chLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black.withAlphaComponent(0.25)
return label
}()
private lazy var categoryView: NRHomeCategoryTagView = {
let view = NRHomeCategoryTagView()
return view
}()
private lazy var collectButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "collect_icon_04"), for: .normal)
button.setImage(UIImage(named: "collect_icon_04_selected"), for: .selected)
button.setImage(UIImage(named: "collect_icon_04_selected"), for: [.selected, .highlighted])
button.addAction(UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let isCollect = !(self.model?.is_collect ?? false)
Task {
await NRNovelAPI.requestCollect(isCollect: isCollect, id: self.model?.id ?? "")
}
}), for: .touchUpInside)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(updateCollectStateNotification), name: NRNovelAPI.updateCollectStateNotification, object: nil)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func updateCollectStateNotification(sender: Notification) {
guard let userInfo = sender.userInfo else { return }
guard let id = userInfo["id"] as? String else { return }
guard let state = userInfo["state"] as? Bool else { return }
guard id == self.model?.id else { return }
self.model?.is_collect = state
collectButton.isSelected = model?.is_collect ?? false
}
}
extension NRNovelHistoryCell {
private func nr_setupUI() {
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(chLabel)
contentView.addSubview(categoryView)
contentView.addSubview(collectButton)
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.bottom.equalToSuperview()
make.width.equalTo(54)
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(12)
make.top.equalToSuperview().offset(8)
make.right.lessThanOrEqualToSuperview().offset(-60)
}
chLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(7)
}
categoryView.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-7.5)
}
collectButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-16)
}
}
}

View File

@ -0,0 +1,96 @@
//
// NRAboutViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
class NRAboutViewController: NRViewController {
private lazy var dataArr: [NRMeItem] = {
let arr = [
NRMeItem(type: .web, title: "Privacy Policy".localized, url: kNRPrivacyPolicyWebUrl),
NRMeItem(type: .web, title: "User Agreement".localized, url: kNRUserAgreementWebUrl),
NRMeItem(type: .web, title: "Visit Website".localized, url: NRWebBaseURL),
]
return arr
}()
private lazy var headerView: NRAboutHeaderView = {
let view = NRAboutHeaderView(frame: .init(x: 0, y: 0, width: UIScreen.width, height: 175))
return view
}()
private lazy var tableView: NRTableView = {
let tableView = NRTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 50
tableView.separatorStyle = .none
tableView.register(NRAboutCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = .top
self.backgroundImageView.isHidden = true
self.title = "About".localized
configNavigationBack("arrow_left_icon_05")
nr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
}
extension NRAboutViewController {
private func nr_setupUI() {
tableView.tableHeaderView = self.headerView
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension NRAboutViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NRAboutCell
cell.item = dataArr[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = dataArr[indexPath.row]
guard let webUrl = item.url else { return }
let vc = NRWebViewController()
vc.title = item.title
vc.webUrl = webUrl
self.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,41 @@
//
// NRHistoryViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/4.
//
import UIKit
import SnapKit
class NRHistoryViewController: NRViewController {
private lazy var vc = NRNovelHistoryViewController()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "History".localized
self.backgroundImageView.isHidden = true
configNavigationBack("arrow_left_icon_05")
addChild(vc)
view.addSubview(vc.view)
vc.view.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
}

Some files were not shown because too many files have changed in this diff Show More