首次提交
This commit is contained in:
parent
320d99b66c
commit
5b2b59ba12
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
37
Podfile
Normal 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
|
||||||
1503
ReaderHive.xcodeproj/project.pbxproj
Normal file
1503
ReaderHive.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||||
40
ReaderHive/Base/Define/NRDefine.swift
Normal file
40
ReaderHive/Base/Define/NRDefine.swift
Normal 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!)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
ReaderHive/Base/Define/NRUserDefaultsKey.swift
Normal file
13
ReaderHive/Base/Define/NRUserDefaultsKey.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// NRUserDefaultsKey.swift
|
||||||
|
// ReaderHive
|
||||||
|
//
|
||||||
|
// Created by 湖北秦九 on 2025/11/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
///登录token
|
||||||
|
let kNRLoginTokenDefaultsKey = "kNRLoginTokenDefaultsKey"
|
||||||
|
///用户信息
|
||||||
|
let kNRUserInfoDefaultsKey = "kNRUserInfoDefaultsKey"
|
||||||
|
///阅读设置
|
||||||
|
let kNRNovelReadSetDefaultsKey = "kNRNovelReadSetDefaultsKey"
|
||||||
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
36
ReaderHive/Base/Extension/NSNumber+NRAdd.swift
Normal file
36
ReaderHive/Base/Extension/NSNumber+NRAdd.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
ReaderHive/Base/Extension/String+NRAdd.swift
Normal file
44
ReaderHive/Base/Extension/String+NRAdd.swift
Normal 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 []}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ReaderHive/Base/Extension/UIFont+NRAdd.swift
Normal file
15
ReaderHive/Base/Extension/UIFont+NRAdd.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
53
ReaderHive/Base/Extension/UINavigationBar+NRAdd.swift
Normal file
53
ReaderHive/Base/Extension/UINavigationBar+NRAdd.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
48
ReaderHive/Base/Extension/UIScreen+NRAdd.swift
Normal file
48
ReaderHive/Base/Extension/UIScreen+NRAdd.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
49
ReaderHive/Base/Extension/UIScrollView+Refresh.swift
Normal file
49
ReaderHive/Base/Extension/UIScrollView+Refresh.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ReaderHive/Base/Extension/UIStackView+NRAdd.swift
Normal file
21
ReaderHive/Base/Extension/UIStackView+NRAdd.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
97
ReaderHive/Base/Extension/UIView+NRAdd.swift
Normal file
97
ReaderHive/Base/Extension/UIView+NRAdd.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
44
ReaderHive/Base/Extension/UserDefaults+NRAdd.swift
Normal file
44
ReaderHive/Base/Extension/UserDefaults+NRAdd.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
ReaderHive/Base/Networking/API/NRHomeAPI.swift
Normal file
144
ReaderHive/Base/Networking/API/NRHomeAPI.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
240
ReaderHive/Base/Networking/API/NRNovelAPI.swift
Normal file
240
ReaderHive/Base/Networking/API/NRNovelAPI.swift
Normal 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")
|
||||||
|
|
||||||
|
}
|
||||||
47
ReaderHive/Base/Networking/API/NRSettingAPI.swift
Normal file
47
ReaderHive/Base/Networking/API/NRSettingAPI.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
ReaderHive/Base/Networking/API/NRUserAPI.swift
Normal file
29
ReaderHive/Base/Networking/API/NRUserAPI.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
201
ReaderHive/Base/Networking/NRNetwork.swift
Normal file
201
ReaderHive/Base/Networking/NRNetwork.swift
Normal 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!)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
ReaderHive/Base/Networking/NRNetworkModel.swift
Normal file
49
ReaderHive/Base/Networking/NRNetworkModel.swift
Normal 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?
|
||||||
|
}
|
||||||
|
}
|
||||||
73
ReaderHive/Base/Networking/NRNetworkReachableManager.swift
Normal file
73
ReaderHive/Base/Networking/NRNetworkReachableManager.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
94
ReaderHive/Base/Networking/NRResponseCryptor.swift
Normal file
94
ReaderHive/Base/Networking/NRResponseCryptor.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
ReaderHive/Base/Networking/NRTargetType.swift
Normal file
90
ReaderHive/Base/Networking/NRTargetType.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ReaderHive/Base/Networking/NRUrlPath.swift
Normal file
20
ReaderHive/Base/Networking/NRUrlPath.swift
Normal 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"
|
||||||
44
ReaderHive/Base/VC/NRNavigationController.swift
Normal file
44
ReaderHive/Base/VC/NRNavigationController.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
59
ReaderHive/Base/VC/NRTabBarController.swift
Normal file
59
ReaderHive/Base/VC/NRTabBarController.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
123
ReaderHive/Base/VC/NRViewController.swift
Normal file
123
ReaderHive/Base/VC/NRViewController.swift
Normal 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
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
ReaderHive/Base/View/NRCollectionView.swift
Normal file
23
ReaderHive/Base/View/NRCollectionView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
ReaderHive/Base/View/NRGradientButton.swift
Normal file
44
ReaderHive/Base/View/NRGradientButton.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
ReaderHive/Base/View/NRGradientView.swift
Normal file
44
ReaderHive/Base/View/NRGradientView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
99
ReaderHive/Base/View/NRImageView.swift
Normal file
99
ReaderHive/Base/View/NRImageView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
ReaderHive/Base/View/NRLabel.swift
Normal file
51
ReaderHive/Base/View/NRLabel.swift
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
79
ReaderHive/Base/View/NRPanModalContentView.swift
Normal file
79
ReaderHive/Base/View/NRPanModalContentView.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
230
ReaderHive/Base/View/NRProgressView.swift
Normal file
230
ReaderHive/Base/View/NRProgressView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
21
ReaderHive/Base/View/NRScrollView.swift
Normal file
21
ReaderHive/Base/View/NRScrollView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
56
ReaderHive/Base/View/NRTableView.swift
Normal file
56
ReaderHive/Base/View/NRTableView.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
ReaderHive/Base/View/NRTableViewCell.swift
Normal file
49
ReaderHive/Base/View/NRTableViewCell.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
157
ReaderHive/Base/WebView/NRWebView.swift
Normal file
157
ReaderHive/Base/WebView/NRWebView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
28
ReaderHive/Base/WebView/NRWebViewController+Script.swift
Normal file
28
ReaderHive/Base/WebView/NRWebViewController+Script.swift
Normal 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
111
ReaderHive/Base/WebView/NRWebViewController.swift
Normal file
111
ReaderHive/Base/WebView/NRWebViewController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
47
ReaderHive/Class/Explore/M/NRExploreNovelMenuItem.swift
Normal file
47
ReaderHive/Class/Explore/M/NRExploreNovelMenuItem.swift
Normal 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?
|
||||||
|
|
||||||
|
}
|
||||||
199
ReaderHive/Class/Explore/V/NRExploreNovelContentListCell.swift
Normal file
199
ReaderHive/Class/Explore/V/NRExploreNovelContentListCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
40
ReaderHive/Class/Explore/V/NRExploreNovelGenresCell.swift
Normal file
40
ReaderHive/Class/Explore/V/NRExploreNovelGenresCell.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
73
ReaderHive/Class/Explore/V/NRExploreNovelMenuCell.swift
Normal file
73
ReaderHive/Class/Explore/V/NRExploreNovelMenuCell.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
101
ReaderHive/Class/Explore/V/NRExploreNovelMenuView.swift
Normal file
101
ReaderHive/Class/Explore/V/NRExploreNovelMenuView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
108
ReaderHive/Class/Explore/V/NRNovelGenresCell.swift
Normal file
108
ReaderHive/Class/Explore/V/NRNovelGenresCell.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
109
ReaderHive/Class/Explore/VC/NRExploreNovelViewController.swift
Normal file
109
ReaderHive/Class/Explore/VC/NRExploreNovelViewController.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
69
ReaderHive/Class/Explore/VC/NRExploreViewController.swift
Normal file
69
ReaderHive/Class/Explore/VC/NRExploreViewController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
144
ReaderHive/Class/Explore/VC/NRNovelGenresViewController.swift
Normal file
144
ReaderHive/Class/Explore/VC/NRNovelGenresViewController.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
ReaderHive/Class/Explore/VM/NRExploreNovelViewModel.swift
Normal file
24
ReaderHive/Class/Explore/VM/NRExploreNovelViewModel.swift
Normal 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
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
188
ReaderHive/Class/Home/C/NRHomeNovelListViewController.swift
Normal file
188
ReaderHive/Class/Home/C/NRHomeNovelListViewController.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
142
ReaderHive/Class/Home/C/NRHomeNovelNewViewController.swift
Normal file
142
ReaderHive/Class/Home/C/NRHomeNovelNewViewController.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
168
ReaderHive/Class/Home/C/NRHomeNovelViewController.swift
Normal file
168
ReaderHive/Class/Home/C/NRHomeNovelViewController.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
68
ReaderHive/Class/Home/C/NRHomeViewController.swift
Normal file
68
ReaderHive/Class/Home/C/NRHomeViewController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
109
ReaderHive/Class/Home/C/NRSearchViewController.swift
Normal file
109
ReaderHive/Class/Home/C/NRSearchViewController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
ReaderHive/Class/Home/M/NRHomeNovelModuleItem.swift
Normal file
76
ReaderHive/Class/Home/M/NRHomeNovelModuleItem.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ReaderHive/Class/Home/M/NRReadWhatViewTransformer.swift
Normal file
20
ReaderHive/Class/Home/M/NRReadWhatViewTransformer.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
53
ReaderHive/Class/Home/V/NRHomeCategoryTagView.swift
Normal file
53
ReaderHive/Class/Home/V/NRHomeCategoryTagView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
69
ReaderHive/Class/Home/V/NRHomeNovelHeaderContentView.swift
Normal file
69
ReaderHive/Class/Home/V/NRHomeNovelHeaderContentView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
172
ReaderHive/Class/Home/V/NRHomeNovelHeaderView.swift
Normal file
172
ReaderHive/Class/Home/V/NRHomeNovelHeaderView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
ReaderHive/Class/Home/V/NRHomeNovelHotGridView.swift
Normal file
23
ReaderHive/Class/Home/V/NRHomeNovelHotGridView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
ReaderHive/Class/Home/V/NRHomeNovelHotTagCell.swift
Normal file
37
ReaderHive/Class/Home/V/NRHomeNovelHotTagCell.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
140
ReaderHive/Class/Home/V/NRHomeNovelHotTagView.swift
Normal file
140
ReaderHive/Class/Home/V/NRHomeNovelHotTagView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
106
ReaderHive/Class/Home/V/NRHomeNovelListCell.swift
Normal file
106
ReaderHive/Class/Home/V/NRHomeNovelListCell.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
121
ReaderHive/Class/Home/V/NRHomeNovelListTextCell.swift
Normal file
121
ReaderHive/Class/Home/V/NRHomeNovelListTextCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
179
ReaderHive/Class/Home/V/NRHomeNovelMustReadTodayCell.swift
Normal file
179
ReaderHive/Class/Home/V/NRHomeNovelMustReadTodayCell.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
ReaderHive/Class/Home/V/NRHomeNovelMustReadTodayView.swift
Normal file
86
ReaderHive/Class/Home/V/NRHomeNovelMustReadTodayView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
128
ReaderHive/Class/Home/V/NRHomeNovelNewArrivalsCell.swift
Normal file
128
ReaderHive/Class/Home/V/NRHomeNovelNewArrivalsCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
84
ReaderHive/Class/Home/V/NRHomeNovelNewArrivalsView.swift
Normal file
84
ReaderHive/Class/Home/V/NRHomeNovelNewArrivalsView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
89
ReaderHive/Class/Home/V/NRHomeNovelNextView.swift
Normal file
89
ReaderHive/Class/Home/V/NRHomeNovelNextView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
128
ReaderHive/Class/Home/V/NRHomeNovelNextViewCell.swift
Normal file
128
ReaderHive/Class/Home/V/NRHomeNovelNextViewCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
ReaderHive/Class/Home/V/NRHomeNovelReadWhatCell.swift
Normal file
50
ReaderHive/Class/Home/V/NRHomeNovelReadWhatCell.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
301
ReaderHive/Class/Home/V/NRHomeNovelReadWhatView.swift
Normal file
301
ReaderHive/Class/Home/V/NRHomeNovelReadWhatView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
88
ReaderHive/Class/Home/V/NRSearchHomeView.swift
Normal file
88
ReaderHive/Class/Home/V/NRSearchHomeView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
ReaderHive/Class/Home/V/NRSearchInputView.swift
Normal file
109
ReaderHive/Class/Home/V/NRSearchInputView.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
40
ReaderHive/Class/Home/V/NRSearchRecordCell.swift
Normal file
40
ReaderHive/Class/Home/V/NRSearchRecordCell.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
136
ReaderHive/Class/Home/V/NRSearchRecordView.swift
Normal file
136
ReaderHive/Class/Home/V/NRSearchRecordView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
137
ReaderHive/Class/Home/V/NRSearchResultCell.swift
Normal file
137
ReaderHive/Class/Home/V/NRSearchResultCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
106
ReaderHive/Class/Home/V/NRSearchResultView.swift
Normal file
106
ReaderHive/Class/Home/V/NRSearchResultView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
118
ReaderHive/Class/Home/V/NRStarGradeView.swift
Normal file
118
ReaderHive/Class/Home/V/NRStarGradeView.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
74
ReaderHive/Class/Home/VM/NRHomeNovelViewModel.swift
Normal file
74
ReaderHive/Class/Home/VM/NRHomeNovelViewModel.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
45
ReaderHive/Class/Home/VM/NRSearchViewModel.swift
Normal file
45
ReaderHive/Class/Home/VM/NRSearchViewModel.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
21
ReaderHive/Class/Me/M/NRLanguageModel.swift
Normal file
21
ReaderHive/Class/Me/M/NRLanguageModel.swift
Normal 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?
|
||||||
|
|
||||||
|
}
|
||||||
25
ReaderHive/Class/Me/M/NRMeItem.swift
Normal file
25
ReaderHive/Class/Me/M/NRMeItem.swift
Normal 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?
|
||||||
|
|
||||||
|
}
|
||||||
49
ReaderHive/Class/Me/V/NRAboutCell.swift
Normal file
49
ReaderHive/Class/Me/V/NRAboutCell.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
82
ReaderHive/Class/Me/V/NRAboutHeaderView.swift
Normal file
82
ReaderHive/Class/Me/V/NRAboutHeaderView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
ReaderHive/Class/Me/V/NRLanguageCell.swift
Normal file
76
ReaderHive/Class/Me/V/NRLanguageCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
68
ReaderHive/Class/Me/V/NRMeCell.swift
Normal file
68
ReaderHive/Class/Me/V/NRMeCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
103
ReaderHive/Class/Me/V/NRMeCoinsContentView.swift
Normal file
103
ReaderHive/Class/Me/V/NRMeCoinsContentView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
ReaderHive/Class/Me/V/NRMeCoinsView.swift
Normal file
76
ReaderHive/Class/Me/V/NRMeCoinsView.swift
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
146
ReaderHive/Class/Me/V/NRMeHeaderView.swift
Normal file
146
ReaderHive/Class/Me/V/NRMeHeaderView.swift
Normal 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)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
134
ReaderHive/Class/Me/V/NRNovelHistoryCell.swift
Normal file
134
ReaderHive/Class/Me/V/NRNovelHistoryCell.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
96
ReaderHive/Class/Me/VC/NRAboutViewController.swift
Normal file
96
ReaderHive/Class/Me/VC/NRAboutViewController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
41
ReaderHive/Class/Me/VC/NRHistoryViewController.swift
Normal file
41
ReaderHive/Class/Me/VC/NRHistoryViewController.swift
Normal 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
Loading…
x
Reference in New Issue
Block a user