首次提交
This commit is contained in:
parent
44bf2c4512
commit
001f392af6
47
.gitignore
vendored
47
.gitignore
vendored
@ -13,6 +13,7 @@ xcuserdata/
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
Podfile.lock
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
@ -32,6 +33,52 @@ playground.xcworkspace
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# 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
|
||||
#
|
||||
Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
*.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# ---> Objective-C
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
|
||||
43
Podfile
Normal file
43
Podfile
Normal file
@ -0,0 +1,43 @@
|
||||
# Uncomment the next line to define a global platform for your project
|
||||
platform :ios, '15.0'
|
||||
|
||||
target 'XSeri' do
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
use_frameworks!
|
||||
|
||||
# Pods for XSeri
|
||||
pod 'SnapKit'
|
||||
pod 'Kingfisher'
|
||||
pod 'Moya'
|
||||
pod 'SmartCodable', '5.0.15'
|
||||
pod 'ESTabBarController-swift'
|
||||
pod 'JXSegmentedView'
|
||||
pod 'collection-view-layouts/TagsLayout'
|
||||
pod 'FSPagerView'
|
||||
pod 'YYCategories'
|
||||
pod 'YYText'
|
||||
pod 'JXPlayer'
|
||||
pod 'LYEmptyView'
|
||||
pod 'FDFullscreenPopGesture'
|
||||
pod 'SVProgressHUD'
|
||||
pod 'Toast'
|
||||
pod 'MJRefresh'
|
||||
pod 'HWPanModal'
|
||||
pod 'ZLPhotoBrowser'
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# 统一设置部署版本
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
||||
|
||||
# 移除可能导致模拟器运行报错的排除架构设置,让 Xcode 自动处理
|
||||
config.build_settings.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
||||
config.build_settings.delete('EXCLUDED_ARCHITECTURES')
|
||||
|
||||
# 禁用 Bitcode (现代项目通常不需要)
|
||||
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
1247
XSeri.xcodeproj/project.pbxproj
Normal file
1247
XSeri.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
23
XSeri/AppScene/AppDelegate+Config.swift
Normal file
23
XSeri/AppScene/AppDelegate+Config.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// AppDelegate+Config.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
import UIKit
|
||||
import MJRefresh
|
||||
|
||||
extension AppDelegate {
|
||||
|
||||
func config() {
|
||||
UIView.xs_awake()
|
||||
XSToast.config()
|
||||
|
||||
MJRefreshConfig.default.languageCode = "en"
|
||||
|
||||
let appearance = UINavigationBarAppearance.defaultAppearance()
|
||||
UINavigationBar.appearance().scrollEdgeAppearance = appearance
|
||||
UINavigationBar.appearance().standardAppearance = appearance
|
||||
}
|
||||
|
||||
}
|
||||
53
XSeri/AppScene/AppDelegate.swift
Normal file
53
XSeri/AppScene/AppDelegate.swift
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
XSNetworkMonitorManager.manager.startMonitoring()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil)
|
||||
self.config()
|
||||
|
||||
Task {
|
||||
await XSLoginManager.manager.updateUserInfo()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
|
||||
@objc private func networkStatusDidChangeNotification() {
|
||||
guard XSNetworkMonitorManager.manager.isReachable == true else { return }
|
||||
Task {
|
||||
await XSLoginManager.manager.updateUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
53
XSeri/AppScene/SceneDelegate.swift
Normal file
53
XSeri/AppScene/SceneDelegate.swift
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// SceneDelegate.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
guard let windowScene = (scene as? UIWindowScene) else { return }
|
||||
XSTool.windowScene = windowScene
|
||||
|
||||
self.window = UIWindow(windowScene: windowScene)
|
||||
self.window?.rootViewController = XSTabBarController()
|
||||
self.window?.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
// Called when the scene has moved from an inactive state to an active state.
|
||||
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// Called when the scene will move from an active state to an inactive state.
|
||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// Use this method to undo the changes made on entering the background.
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
28
XSeri/Base/Constants/XSConfig.swift
Normal file
28
XSeri/Base/Constants/XSConfig.swift
Normal file
@ -0,0 +1,28 @@
|
||||
import UIKit
|
||||
|
||||
/// 全局样式常量配置
|
||||
struct XSConfig {
|
||||
|
||||
struct Color {
|
||||
/// TabBar 背景色 (#0F0F0F)
|
||||
static let tabBarBg = UIColor(red: 0.059, green: 0.059, blue: 0.059, alpha: 1.0)
|
||||
|
||||
/// TabBar 选中颜色 (#FFDAA4)
|
||||
static let tabBarActive = UIColor(red: 1.0, green: 0.855, blue: 0.643, alpha: 1.0)
|
||||
|
||||
/// TabBar 未选中颜色 (白色 40% 透明度)
|
||||
static let tabBarInactive = UIColor.white.withAlphaComponent(0.4)
|
||||
|
||||
/// TabBar 分割线颜色 (白色 4% 透明度)
|
||||
static let tabBarSeparator = UIColor.white.withAlphaComponent(0.04)
|
||||
}
|
||||
|
||||
struct Font {
|
||||
/// TabBar 选中字体 (Medium 12pt)
|
||||
static let tabBarActive = UIFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
/// TabBar 未选中字体 (Regular 11pt)
|
||||
static let tabBarInactive = UIFont.systemFont(ofSize: 11, weight: .regular)
|
||||
}
|
||||
|
||||
}
|
||||
47
XSeri/Base/Constants/XSDefine.swift
Normal file
47
XSeri/Base/Constants/XSDefine.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// XSDefine.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
///当前系统版本号
|
||||
let kXSSystemVersion: String = UIDevice.current.systemVersion
|
||||
let kXSBundleIdentifier: String = (Bundle.main.infoDictionary!["CFBundleIdentifier"] as? String) ?? "0"
|
||||
|
||||
///app版本号
|
||||
let kXSVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0"
|
||||
let kXSBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0"
|
||||
|
||||
let kXSName: String = (Bundle.main.infoDictionary!["CFBundleDisplayName"] as? String) ?? ""
|
||||
let kXSBundleName: String = (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? ""
|
||||
|
||||
|
||||
#if DEBUG
|
||||
func xsLog(_ msg: Any?, file: String = #file, function: String = #function, line: Int = #line) {
|
||||
if let msg = msg {
|
||||
print("\n\(Date(timeIntervalSinceNow: 8 * 60 * 60)) \(file.components(separatedBy: "/").last ?? "") \(function) \(line): \(msg)")
|
||||
}
|
||||
}
|
||||
#else
|
||||
func xsLog(_ msg: Any?) { }
|
||||
#endif
|
||||
|
||||
public func xs_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!)
|
||||
}
|
||||
}
|
||||
51
XSeri/Base/Constants/XSScreen.swift
Normal file
51
XSeri/Base/Constants/XSScreen.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// XSScreen.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct XSScreen {
|
||||
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 XSTool.keyWindow?.safeAreaInsets.top ?? 20
|
||||
}
|
||||
|
||||
static var safeBottom: CGFloat {
|
||||
return XSTool.keyWindow?.safeAreaInsets.bottom ?? 0
|
||||
}
|
||||
|
||||
static var navBarHeight: CGFloat {
|
||||
return safeTop + 44
|
||||
}
|
||||
|
||||
static var tabBarHeight: CGFloat {
|
||||
return safeBottom + 49
|
||||
}
|
||||
|
||||
static var customTabBarHeight: CGFloat {
|
||||
return safeBottom + 60
|
||||
}
|
||||
|
||||
///屏幕宽比
|
||||
static var widthRatio: CGFloat {
|
||||
return width / 375
|
||||
}
|
||||
|
||||
static func getRatioWidth(size: CGFloat) -> CGFloat {
|
||||
return self.widthRatio * size
|
||||
}
|
||||
}
|
||||
11
XSeri/Base/Constants/XSUserDefaultsKey.swift
Normal file
11
XSeri/Base/Constants/XSUserDefaultsKey.swift
Normal file
@ -0,0 +1,11 @@
|
||||
//
|
||||
// XSUserDefaultsKey.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
///登录token
|
||||
let kXSLoginTokenDefaultsKey = "kXSLoginTokenDefaultsKey"
|
||||
///用户信息
|
||||
let kXSUserInfoDefaultsKey = "kXSUserInfoDefaultsKey"
|
||||
32
XSeri/Base/Controller/XSCommonViewController.swift
Normal file
32
XSeri/Base/Controller/XSCommonViewController.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// XSCommonViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSCommonViewController: XSViewController {
|
||||
|
||||
|
||||
private(set) lazy var bgImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "bg_image_01"))
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
view.addSubview(bgImageView)
|
||||
|
||||
bgImageView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
47
XSeri/Base/Controller/XSNavigationController.swift
Normal file
47
XSeri/Base/Controller/XSNavigationController.swift
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// XSNavigationController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import FDFullscreenPopGesture
|
||||
|
||||
class XSNavigationController: UINavigationController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
if #available(iOS 26.0, *) {
|
||||
self.interactiveContentPopGestureRecognizer?.isEnabled = false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
101
XSeri/Base/Controller/XSTabBarController.swift
Normal file
101
XSeri/Base/Controller/XSTabBarController.swift
Normal file
@ -0,0 +1,101 @@
|
||||
//
|
||||
// XSTabBarController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ESTabBarController_swift
|
||||
|
||||
class XSTabBarController: ESTabBarController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.view.backgroundColor = .black
|
||||
|
||||
setupCustomTabBar()
|
||||
setupTabBarAppearance()
|
||||
setupViewControllers()
|
||||
}
|
||||
|
||||
/// 使用自定义 TabBar,从根源控制高度(系统会根据 sizeThatFits 进行布局)
|
||||
private func setupCustomTabBar() {
|
||||
let tabBar = XSCustomTabBar()
|
||||
tabBar.delegate = self
|
||||
self.setValue(tabBar, forKey: "tabBar")
|
||||
}
|
||||
|
||||
/// 配置 TabBar 外观样式
|
||||
private func setupTabBarAppearance() {
|
||||
// 设置背景颜色
|
||||
self.tabBar.backgroundColor = XSConfig.Color.tabBarBg
|
||||
|
||||
// 设置顶部分割线颜色 (使用 XSConfig 中的 white 4%)
|
||||
self.tabBar.shadowImage = UIImage()
|
||||
self.tabBar.backgroundImage = UIImage()
|
||||
|
||||
// 设置 TabBar 容器样式:顶部 20pt 圆角
|
||||
self.tabBar.layer.cornerRadius = 20
|
||||
self.tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
self.tabBar.clipsToBounds = true
|
||||
|
||||
// 添加顶部分割线视图,因为 ESTabBar 可能会覆盖自定义 shadow
|
||||
let line = UIView()
|
||||
line.backgroundColor = XSConfig.Color.tabBarSeparator
|
||||
self.tabBar.addSubview(line)
|
||||
line.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 0.5)
|
||||
}
|
||||
|
||||
/// 初始化子控制器
|
||||
private func setupViewControllers() {
|
||||
let homeVC = createNavController(
|
||||
rootVC: XSHomeViewController(),
|
||||
title: "Home".localized,
|
||||
imageName: "tab_home_unsel",
|
||||
selectedImageName: "tab_home_sel"
|
||||
)
|
||||
|
||||
let discoverVC = createNavController(
|
||||
rootVC: XSDiscoverViewController(),
|
||||
title: "Discover".localized,
|
||||
imageName: "tab_discover_unsel",
|
||||
selectedImageName: "tab_discover_sel"
|
||||
)
|
||||
|
||||
let myListVC = createNavController(
|
||||
rootVC: XSMyListViewController(),
|
||||
title: "My List".localized,
|
||||
imageName: "tab_list_unsel",
|
||||
selectedImageName: "tab_list_sel"
|
||||
)
|
||||
|
||||
let portfolioVC = createNavController(
|
||||
rootVC: XSMineViewController(),
|
||||
title: "Portfolio".localized,
|
||||
imageName: "tab_portfolio_unsel",
|
||||
selectedImageName: "tab_portfolio_sel"
|
||||
)
|
||||
|
||||
viewControllers = [homeVC, discoverVC, myListVC, portfolioVC]
|
||||
}
|
||||
|
||||
/// 创建导航控制器并配置 ESTabBarItem
|
||||
private func createNavController(rootVC: UIViewController, title: String, imageName: String, selectedImageName: String) -> UINavigationController {
|
||||
|
||||
let nav = XSNavigationController(rootViewController: rootVC)
|
||||
|
||||
let itemContents = XSTabBarItemContentView()
|
||||
|
||||
// 配置图标和标题
|
||||
let item = ESTabBarItem(
|
||||
itemContents,
|
||||
title: title,
|
||||
image: UIImage(named: imageName),
|
||||
selectedImage: UIImage(named: selectedImageName)
|
||||
)
|
||||
|
||||
nav.tabBarItem = item
|
||||
return nav
|
||||
}
|
||||
}
|
||||
94
XSeri/Base/Controller/XSViewController.swift
Normal file
94
XSeri/Base/Controller/XSViewController.swift
Normal file
@ -0,0 +1,94 @@
|
||||
//
|
||||
// XSViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import JXSegmentedView
|
||||
|
||||
class XSViewController: UIViewController {
|
||||
|
||||
var backImage: UIImage? {
|
||||
return UIImage(named: "arrow_left_icon_03")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = ._0_F_0_F_0_F
|
||||
|
||||
if let navi = navigationController {
|
||||
if navi.visibleViewController == self {
|
||||
if navi.viewControllers.count > 1 {
|
||||
configNavigationBack(image: backImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||
completer?()
|
||||
}
|
||||
|
||||
func handleFooterRefresh(_ completer: (() -> Void)?) {
|
||||
completer?()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
func configNavigationBack(image: UIImage?) {
|
||||
if let image = image {
|
||||
let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleNavigationBack))
|
||||
navigationItem.leftBarButtonItem = leftBarButtonItem
|
||||
} else {
|
||||
navigationItem.leftBarButtonItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleNavigationBack() {
|
||||
self.xs_toLastViewController(animated: true)
|
||||
}
|
||||
|
||||
func xs_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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//MARK: JXSegmentedListContainerViewListDelegate
|
||||
extension XSViewController: JXSegmentedListContainerViewListDelegate {
|
||||
func listView() -> UIView {
|
||||
return self.view
|
||||
}
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
|
||||
func xs_setNavigationStyle(backgroundColor: UIColor = .clear,
|
||||
titleFont: UIFont = .font(ofSize: 18, weight: .bold),
|
||||
titleColor: UIColor = .white,
|
||||
isTranslucent: Bool = true
|
||||
) {
|
||||
self.navigationController?.navigationBar.xs_setTranslucent(isTranslucent)
|
||||
self.navigationController?.navigationBar.xs_setBackgroundColor(backgroundColor)
|
||||
self.navigationController?.navigationBar.xs_setTitleTextAttributes([
|
||||
NSAttributedString.Key.font : titleFont,
|
||||
NSAttributedString.Key.foregroundColor : titleColor
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
65
XSeri/Base/Extension/CGMutablePath+XS.swift
Normal file
65
XSeri/Base/Extension/CGMutablePath+XS.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// CGMutablePath+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct XSRoundedCorner {
|
||||
var topLeft:CGFloat = 0
|
||||
var topRight:CGFloat = 0
|
||||
var bottomLeft:CGFloat = 0
|
||||
var bottomRight:CGFloat = 0
|
||||
|
||||
public static let zero = XSRoundedCorner(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:XSRoundedCorner, v2:XSRoundedCorner) -> Bool {
|
||||
return v1.bottomLeft == v2.bottomLeft
|
||||
&& v1.bottomRight == v2.bottomRight
|
||||
&& v1.topLeft == v2.topLeft
|
||||
&& v1.topRight == v2.topRight
|
||||
}
|
||||
static func !=(v1:XSRoundedCorner, v2:XSRoundedCorner) -> Bool {
|
||||
return !(v1 == v2)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGMutablePath {
|
||||
func addRadiusRectangle(_ circulars: XSRoundedCorner, 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();
|
||||
|
||||
}
|
||||
}
|
||||
23
XSeri/Base/Extension/Dictionary+XS.swift
Normal file
23
XSeri/Base/Extension/Dictionary+XS.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Dictionary+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Dictionary {
|
||||
|
||||
func toJsonString() -> String? {
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: self)
|
||||
let jsonStr = String(data: data, encoding: .utf8)
|
||||
return jsonStr
|
||||
} catch {
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
35
XSeri/Base/Extension/NSNumber+XS.swift
Normal file
35
XSeri/Base/Extension/NSNumber+XS.swift
Normal file
@ -0,0 +1,35 @@
|
||||
//
|
||||
// NSNumber+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/26.
|
||||
//
|
||||
|
||||
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 format() -> String {
|
||||
let num = self.floatValue
|
||||
|
||||
if num > 1000 {
|
||||
return NSNumber(value: num / 1000).toString(maximumFractionDigits: 1) + "K"
|
||||
} else {
|
||||
return self.stringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
30
XSeri/Base/Extension/String+XS.swift
Normal file
30
XSeri/Base/Extension/String+XS.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// String+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SmartCodable
|
||||
|
||||
extension String: SmartCodable {
|
||||
|
||||
var localized: String {
|
||||
var text = NSLocalizedString(self, comment: "")
|
||||
text = text.replacingOccurrences(of: "<br>", with: "\n")
|
||||
return text
|
||||
}
|
||||
|
||||
func localizedReplace(text: String?) -> String {
|
||||
return self.localized.replacingOccurrences(of: "##", with: text ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
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)
|
||||
}
|
||||
}
|
||||
16
XSeri/Base/Extension/UIFont+XS.swift
Normal file
16
XSeri/Base/Extension/UIFont+XS.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// UIFont+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIFont {
|
||||
|
||||
static func font(ofSize fontSize: CGFloat, weight: Weight) -> UIFont {
|
||||
return UIFont.systemFont(ofSize: fontSize, weight: weight)
|
||||
}
|
||||
|
||||
}
|
||||
49
XSeri/Base/Extension/UINavigationBar+XS.swift
Normal file
49
XSeri/Base/Extension/UINavigationBar+XS.swift
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// UINavigationBar+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
extension UINavigationBarAppearance {
|
||||
static func defaultAppearance() -> UINavigationBarAppearance {
|
||||
let navBarAppearance = UINavigationBarAppearance()
|
||||
navBarAppearance.configureWithOpaqueBackground()
|
||||
navBarAppearance.backgroundColor = .clear
|
||||
navBarAppearance.backgroundEffect = nil
|
||||
navBarAppearance.shadowColor = UIColor.clear
|
||||
navBarAppearance.titleTextAttributes = [
|
||||
NSAttributedString.Key.font : UIFont.font(ofSize: 18, weight: .bold),
|
||||
NSAttributedString.Key.foregroundColor : UIColor.white
|
||||
]
|
||||
return navBarAppearance
|
||||
}
|
||||
}
|
||||
|
||||
extension UINavigationBar {
|
||||
|
||||
func xs_setTranslucent(_ isTranslucent: Bool) {
|
||||
self.isTranslucent = isTranslucent
|
||||
}
|
||||
|
||||
func xs_setBackgroundColor(_ backgroundColor: UIColor?) {
|
||||
let appearance = self.standardAppearance
|
||||
appearance.backgroundColor = backgroundColor
|
||||
self.standardAppearance = appearance
|
||||
self.scrollEdgeAppearance = appearance
|
||||
}
|
||||
|
||||
func xs_setTitleTextAttributes(_ titleTextAttributes: [NSAttributedString.Key : Any]?) {
|
||||
let appearance = self.standardAppearance
|
||||
|
||||
if let titleTextAttributes = titleTextAttributes {
|
||||
appearance.titleTextAttributes = titleTextAttributes
|
||||
}
|
||||
self.scrollEdgeAppearance = appearance
|
||||
self.standardAppearance = appearance
|
||||
|
||||
}
|
||||
}
|
||||
34
XSeri/Base/Extension/UIScrollView+Refresh.swift
Normal file
34
XSeri/Base/Extension/UIScrollView+Refresh.swift
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// UIScrollView+Refresh.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/25.
|
||||
//
|
||||
|
||||
import MJRefresh
|
||||
|
||||
extension UIScrollView {
|
||||
|
||||
func xs_addRefreshHeader(insetTop: CGFloat = 0, block: (() -> Void)?) {
|
||||
|
||||
self.mj_header = MJRefreshNormalHeader(refreshingBlock: {
|
||||
block?()
|
||||
})
|
||||
self.mj_header?.ignoredScrollViewContentInsetTop = insetTop
|
||||
}
|
||||
|
||||
func xs_addRefreshFooter(insetBottom: CGFloat = 0, block: (() -> Void)?) {
|
||||
self.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: {
|
||||
block?()
|
||||
})
|
||||
self.mj_footer?.ignoredScrollViewContentInsetBottom = insetBottom
|
||||
}
|
||||
|
||||
func xs_endHeaderRefreshing() {
|
||||
self.mj_header?.endRefreshing()
|
||||
}
|
||||
|
||||
func xs_endFooterRefreshing() {
|
||||
self.mj_footer?.endRefreshing()
|
||||
}
|
||||
}
|
||||
22
XSeri/Base/Extension/UIStackView+XS.swift
Normal file
22
XSeri/Base/Extension/UIStackView+XS.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// UIStackView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/25.
|
||||
//
|
||||
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIStackView {
|
||||
|
||||
func xs_removeAllArrangedSubview() {
|
||||
let arrangedSubviews = self.arrangedSubviews
|
||||
|
||||
arrangedSubviews.forEach {
|
||||
self.removeArrangedSubview($0)
|
||||
$0.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
65
XSeri/Base/Extension/UIView+XS.swift
Normal file
65
XSeri/Base/Extension/UIView+XS.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// UIView+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIView {
|
||||
|
||||
fileprivate struct AssociatedKeys {
|
||||
static var xs_roundedCorner: Int?
|
||||
static var xs_effect: Int?
|
||||
}
|
||||
|
||||
@objc public static func xs_awake() {
|
||||
xs_swizzled_instanceMethod("xs", oldClass: self, oldSelector: "layoutSubviews", newClass: self)
|
||||
}
|
||||
|
||||
@objc func xs_layoutSubviews() {
|
||||
xs_layoutSubviews()
|
||||
|
||||
xs_updateRoundedCorner()
|
||||
//
|
||||
// if let effectView = effectView, effectView.frame != self.bounds {
|
||||
// effectView.frame = self.bounds
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: -------------- 圆角 --------------
|
||||
extension UIView {
|
||||
|
||||
|
||||
private var roundedCorner: XSRoundedCorner? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.xs_roundedCorner) as? XSRoundedCorner
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.xs_roundedCorner, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
///设置圆角
|
||||
func xs_setCornerRadius(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) {
|
||||
//清空其它设置方法
|
||||
self.roundedCorner = XSRoundedCorner(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
|
||||
xs_updateRoundedCorner()
|
||||
}
|
||||
|
||||
func xs_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
|
||||
}
|
||||
|
||||
}
|
||||
44
XSeri/Base/Extension/UserDefaults+XS.swift
Normal file
44
XSeri/Base/Extension/UserDefaults+XS.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// UserDefaults+XS.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
static func xs_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)")
|
||||
}
|
||||
}
|
||||
|
||||
static func xs_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)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
105
XSeri/Base/Networking/API/XSHomeAPI.swift
Normal file
105
XSeri/Base/Networking/API/XSHomeAPI.swift
Normal file
@ -0,0 +1,105 @@
|
||||
//
|
||||
// XSHomeAPI.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct XSHomeAPI {
|
||||
|
||||
static func requestHomeData() async -> [XSHomeModuleItem]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/home/all-modules")
|
||||
parameters.method = .get
|
||||
parameters.isToast = true
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSHomeModuleItem>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
|
||||
}
|
||||
|
||||
static func requestHomeNew(page: Int) async -> [XSShortModel]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/newShortPlayList")
|
||||
parameters.method = .get
|
||||
parameters.parameters = [
|
||||
"current_page" : page,
|
||||
"page_size" : 20
|
||||
]
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
}
|
||||
|
||||
static func requestHomeRankings() async -> [XSShortModel]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/nDayMaxRechargeShortPlayRank")
|
||||
parameters.method = .get
|
||||
parameters.parameters = [
|
||||
"day" : 30,
|
||||
]
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
}
|
||||
|
||||
static func requestCategoryList() async -> [XSCategoryModel]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/getCategories")
|
||||
parameters.method = .get
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSCategoryModel>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
}
|
||||
|
||||
static func requestCategoryVideo(id: String, page: Int) async -> [XSShortModel]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/videoList")
|
||||
parameters.method = .get
|
||||
parameters.parameters = [
|
||||
"category_id" : id,
|
||||
"current_page" : page,
|
||||
"page_size" : 20
|
||||
]
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
}
|
||||
|
||||
static func requestDiscoverData(page: Int) async -> [XSShortModel]? {
|
||||
|
||||
var parameters = XSNetwork.Parameters(path: "/getRecommands")
|
||||
parameters.method = .get
|
||||
parameters.parameters = [
|
||||
"current_page" : page,
|
||||
"page_size" : 20
|
||||
]
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
|
||||
}
|
||||
|
||||
///搜索
|
||||
static func requestSearch(text: String) async -> [XSShortModel]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/search")
|
||||
parameters.method = .get
|
||||
parameters.parameters = [
|
||||
"search" : text
|
||||
]
|
||||
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
}
|
||||
|
||||
static func requestHotSearchList() async -> [XSShortModel]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/search/hots")
|
||||
parameters.method = .get
|
||||
parameters.isLoding = false
|
||||
parameters.isToast = false
|
||||
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data?.list
|
||||
}
|
||||
|
||||
static func requestTopSearchList() async -> [XSShortModel]? {
|
||||
var parameters = XSNetwork.Parameters(path: "/getVisitTop")
|
||||
parameters.method = .get
|
||||
parameters.isLoding = false
|
||||
parameters.isToast = false
|
||||
|
||||
let response: XSNetwork.Response<[XSShortModel]> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
20
XSeri/Base/Networking/API/XSSettingAPI.swift
Normal file
20
XSeri/Base/Networking/API/XSSettingAPI.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// XSSettingAPI.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/28.
|
||||
//
|
||||
|
||||
|
||||
struct XSSettingAPI {
|
||||
|
||||
static func requestFeedbackRedCount() async -> XSFeedbackCountModel? {
|
||||
|
||||
var param = XSNetwork.Parameters(path: "/noticeNum")
|
||||
param.method = .post
|
||||
param.isLoding = false
|
||||
param.isToast = false
|
||||
let response: XSNetwork.Response<XSFeedbackCountModel> = await XSNetwork.request(parameters: param)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
20
XSeri/Base/Networking/API/XSUserAPI.swift
Normal file
20
XSeri/Base/Networking/API/XSUserAPI.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// XSUserAPI.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
import UIKit
|
||||
|
||||
|
||||
struct XSUserAPI {
|
||||
|
||||
static func requestUserInfo() async -> XSUserInfo? {
|
||||
|
||||
var parameters = XSNetwork.Parameters(path: "/customer/info")
|
||||
parameters.method = .get
|
||||
|
||||
let response: XSNetwork.Response<XSUserInfo> = await XSNetwork.request(parameters: parameters)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
116
XSeri/Base/Networking/API/XSVideoAPI.swift
Normal file
116
XSeri/Base/Networking/API/XSVideoAPI.swift
Normal file
@ -0,0 +1,116 @@
|
||||
//
|
||||
// XSVideoAPI.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
struct XSVideoAPI {
|
||||
|
||||
static func requestShortDetail(shortPlayId: String) async -> (XSShortDetailModel?, Int?, String?) {
|
||||
let parameters: [String : Any] = [
|
||||
"short_play_id" : shortPlayId,
|
||||
"video_id" : "0"
|
||||
]
|
||||
var param = XSNetwork.Parameters(path: "/getVideoDetails")
|
||||
param.method = .get
|
||||
param.isToast = true
|
||||
param.parameters = parameters
|
||||
let response: XSNetwork.Response<XSShortDetailModel> = await XSNetwork.request(parameters: param)
|
||||
if response.isSuccess {
|
||||
return (response.data, response.code, response.msg)
|
||||
} else {
|
||||
return (nil, response.code, response.msg)
|
||||
}
|
||||
}
|
||||
|
||||
static func requestAddPlayHistory(shortPlayId: String?, videoId: String?) async {
|
||||
let parameters: [String : Any] = [
|
||||
"short_play_id" : shortPlayId ?? "0",
|
||||
"video_id" : videoId ?? "0"
|
||||
]
|
||||
var param = XSNetwork.Parameters(path: "/createHistory")
|
||||
param.method = .post
|
||||
param.isToast = false
|
||||
param.isLoding = false
|
||||
param.parameters = parameters
|
||||
let _: XSNetwork.Response<String> = await XSNetwork.request(parameters: param)
|
||||
}
|
||||
|
||||
static func requestPlayHistorys(page: Int, pageSize: Int = 20) async -> [XSShortModel]? {
|
||||
let parameters: [String : Any] = [
|
||||
"current_page" : page,
|
||||
"page_size" : pageSize
|
||||
]
|
||||
var param = XSNetwork.Parameters(path: "/myHistorys")
|
||||
param.method = .get
|
||||
param.parameters = parameters
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: param)
|
||||
if response.isSuccess {
|
||||
return response.data?.list
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func requestCollectShort(isCollect: Bool, shortId: String, videoId: String?) async -> Bool {
|
||||
let path: String
|
||||
if isCollect {
|
||||
path = "/collect"
|
||||
} else {
|
||||
path = "/cancelCollect"
|
||||
}
|
||||
|
||||
var parameters: [String : Any] = [
|
||||
"short_play_id" : shortId,
|
||||
]
|
||||
|
||||
if let videoId = videoId {
|
||||
parameters["video_id"] = videoId
|
||||
}
|
||||
|
||||
var param = XSNetwork.Parameters(path: path)
|
||||
param.method = .post
|
||||
param.isLoding = true
|
||||
param.parameters = parameters
|
||||
let response: XSNetwork.Response<String> = await XSNetwork.request(parameters: param)
|
||||
if response.isSuccess {
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: XSVideoAPI.updateShortCollectStateNotification, object: nil, userInfo: [
|
||||
"state" : isCollect,
|
||||
"id" : shortId,
|
||||
])
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func requestCollectList(page: Int, pageSize: Int = 20) async -> [XSShortModel]? {
|
||||
let parameters: [String : Any] = [
|
||||
"current_page" : page,
|
||||
"page_size" : pageSize
|
||||
]
|
||||
var param = XSNetwork.Parameters(path: "/myCollections")
|
||||
param.method = .get
|
||||
param.parameters = parameters
|
||||
let response: XSNetwork.Response<XSNetwork.List<XSShortModel>> = await XSNetwork.request(parameters: param)
|
||||
if response.isSuccess {
|
||||
return response.data?.list
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSVideoAPI {
|
||||
|
||||
///更新短剧关注状态 [ "state" : isFavorite, "id" : shortPlayId,]
|
||||
static let updateShortCollectStateNotification = Notification.Name(rawValue: "XSVideoAPI.updateShortCollectStateNotification")
|
||||
|
||||
|
||||
}
|
||||
102
XSeri/Base/Networking/Base/XSCryptorService.swift
Normal file
102
XSeri/Base/Networking/Base/XSCryptorService.swift
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// XSCryptorService.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct XSCryptorService {
|
||||
|
||||
// 定义常量
|
||||
static let EN_STR_TAG: String = "$" // 替换为实际的加密标记
|
||||
|
||||
// 解密字符串
|
||||
static func decrypt(data: String) -> String {
|
||||
guard data.hasPrefix(EN_STR_TAG) else {
|
||||
// fatalError("Invalid encoded string")
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
186
XSeri/Base/Networking/Base/XSNetwork.swift
Normal file
186
XSeri/Base/Networking/Base/XSNetwork.swift
Normal file
@ -0,0 +1,186 @@
|
||||
//
|
||||
// XSNetwork.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Moya
|
||||
import SmartCodable
|
||||
|
||||
let kXSRegisterPath = "/customer/register"
|
||||
|
||||
/// 网络请求核心类
|
||||
struct XSNetwork {
|
||||
|
||||
/// Moya 提供者,注入 HUD 插件实现解耦
|
||||
static let provider = MoyaProvider<XSNetwork.Target>(plugins: [XSNetworkHUDPlugin()])
|
||||
|
||||
/// 用于存储正在进行的 Token 刷新任务,防止并发竞争
|
||||
/// 使用 _Concurrency.Task 明确指定系统并发任务,避免与 Moya.Task 冲突
|
||||
private static var refreshTokenTask: _Concurrency.Task<Bool, Never>?
|
||||
|
||||
// MARK: - Public Request Method
|
||||
|
||||
/// 异步网络请求方法
|
||||
/// - Parameter parameters: 请求参数配置
|
||||
/// - Returns: 封装好的 Response 模型
|
||||
static func request<T: SmartCodable>(parameters: XSNetwork.Parameters) async -> XSNetwork.Response<T> {
|
||||
|
||||
// 1. Token 检查 (非注册接口且无 Token 时,先尝试获取)
|
||||
if XSLoginManager.manager.token == nil && parameters.path != kXSRegisterPath {
|
||||
_ = await self.requestToken()
|
||||
}
|
||||
|
||||
// 2. 执行请求
|
||||
let result = await provider.requestAsync(.request(parameters: parameters))
|
||||
|
||||
// 3. 结果处理
|
||||
switch result {
|
||||
case .success(let response):
|
||||
let code = response.statusCode
|
||||
|
||||
// 4. Token 失效处理 (401, 402, 403)
|
||||
if (code == 401 || code == 402 || code == 403) && parameters.path != kXSRegisterPath {
|
||||
|
||||
// 如果是 402 且需要提示
|
||||
if code == 402 && parameters.isToast {
|
||||
await MainActor.run { XSToast.show("network_error_1".localized) }
|
||||
}
|
||||
|
||||
// 尝试刷新 Token
|
||||
let success = await self.requestToken()
|
||||
if success {
|
||||
// 刷新成功后重试当前请求
|
||||
return await self.request(parameters: parameters)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 正常业务数据解析
|
||||
return self.handleResponse(response, parameters: parameters)
|
||||
|
||||
case .failure(let error):
|
||||
// 6. 网络连接层错误处理
|
||||
xsLog("Network Error: \(error)")
|
||||
if parameters.isToast {
|
||||
await MainActor.run { XSToast.show("network_error_2".localized) }
|
||||
}
|
||||
return self.createErrorResponse(msg: "network_error_2".localized)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Helper Methods
|
||||
|
||||
/// 处理响应数据
|
||||
private static func handleResponse<T: SmartCodable>(_ response: Moya.Response, parameters: XSNetwork.Parameters) -> XSNetwork.Response<T> {
|
||||
do {
|
||||
let tempData = try response.mapString()
|
||||
xsLog("Path: \(parameters.path)")
|
||||
xsLog("Params: \(parameters.parameters ?? [:])")
|
||||
|
||||
let networkResponse: XSNetwork.Response<T> = self._deserialize(data: tempData)
|
||||
|
||||
// 业务逻辑错误提示 (例如 code != 200)
|
||||
if !networkResponse.isSuccess && parameters.isToast {
|
||||
let msg = networkResponse.msg ?? "Error".localized
|
||||
DispatchQueue.main.async {
|
||||
XSToast.show(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return networkResponse
|
||||
|
||||
} catch {
|
||||
if parameters.isToast {
|
||||
DispatchQueue.main.async { XSToast.show("Error".localized) }
|
||||
}
|
||||
return self.createErrorResponse(msg: "Error".localized)
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析与解密数据
|
||||
private static func _deserialize<T: SmartCodable>(data: String) -> XSNetwork.Response<T> {
|
||||
let decrypted = XSCryptorService.decrypt(data: data)
|
||||
xsLog("Decrypted Response: \(decrypted)")
|
||||
|
||||
var response = XSNetwork.Response<T>.deserialize(from: decrypted)
|
||||
response?.rawData = decrypted
|
||||
|
||||
if let response = response {
|
||||
return response
|
||||
} else {
|
||||
return self.createErrorResponse(msg: "Error".localized)
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建错误响应对象
|
||||
private static func createErrorResponse<T: SmartCodable>(code: Int = -1, msg: String) -> XSNetwork.Response<T> {
|
||||
var res = XSNetwork.Response<T>()
|
||||
res.code = code
|
||||
res.msg = msg
|
||||
return res
|
||||
}
|
||||
|
||||
// MARK: - Token Management
|
||||
|
||||
/// 获取/刷新 Token
|
||||
@discardableResult
|
||||
@MainActor
|
||||
static func requestToken() async -> Bool {
|
||||
// 如果已经有一个刷新任务在进行,直接等待并返回它的结果
|
||||
if let existingTask = refreshTokenTask {
|
||||
return await existingTask.value
|
||||
}
|
||||
|
||||
// 创建新的刷新任务,明确指定 _Concurrency.Task
|
||||
let task = _Concurrency.Task { () -> Bool in
|
||||
let param = XSNetwork.Parameters(path: kXSRegisterPath, isLoding: false, isToast: false)
|
||||
let response: XSNetwork.Response<XSLoginToken> = await self.request(parameters: param)
|
||||
|
||||
if let token = response.data {
|
||||
XSLoginManager.manager.setAccountToken(token)
|
||||
_Concurrency.Task {
|
||||
await XSLoginManager.manager.updateUserInfo()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
self.refreshTokenTask = task
|
||||
|
||||
// 等待任务完成
|
||||
let result = await task.value
|
||||
|
||||
// 任务完成后重置,以便后续再次刷新
|
||||
self.refreshTokenTask = nil
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Moya Async Extension
|
||||
extension MoyaProvider {
|
||||
/// 封装 Moya 为 Async/Await 风格
|
||||
func requestAsync(_ target: Target) async -> Result<Moya.Response, MoyaError> {
|
||||
await withCheckedContinuation { continuation in
|
||||
self.request(target) { result in
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Backward Compatibility
|
||||
extension XSNetwork {
|
||||
/// 保留旧的闭包回调方法,内部调用 async 方法,方便平滑迁移
|
||||
static func request<T: SmartCodable>(parameters: XSNetwork.Parameters, completion: ((_ response: XSNetwork.Response<T>) -> Void)?) {
|
||||
_Concurrency.Task {
|
||||
let response: XSNetwork.Response<T> = await self.request(parameters: parameters)
|
||||
await MainActor.run {
|
||||
completion?(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
XSeri/Base/Networking/Base/XSNetworkModel.swift
Normal file
75
XSeri/Base/Networking/Base/XSNetworkModel.swift
Normal file
@ -0,0 +1,75 @@
|
||||
//
|
||||
// XSNetworkModel.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import SmartCodable
|
||||
import Alamofire
|
||||
|
||||
extension XSNetwork {
|
||||
|
||||
/// 业务成功状态码
|
||||
static let SuccessCode = 200
|
||||
|
||||
/// 请求参数配置模型
|
||||
struct Parameters {
|
||||
var path: String
|
||||
var parameters: [String : Any]?
|
||||
var method: Moya.Method = .post
|
||||
var isLoding: Bool = false
|
||||
var isToast: Bool = true
|
||||
|
||||
init(path: String,
|
||||
parameters: [String : Any]? = nil,
|
||||
method: Moya.Method = .post,
|
||||
isLoding: Bool = false,
|
||||
isToast: Bool = true) {
|
||||
self.path = path
|
||||
self.parameters = parameters
|
||||
self.method = method
|
||||
self.isLoding = isLoding
|
||||
self.isToast = isToast
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一响应模型
|
||||
struct Response<T: SmartCodable>: SmartCodable {
|
||||
var data: T?
|
||||
var code: Int?
|
||||
var msg: String?
|
||||
|
||||
/// 原始解密数据,用于特殊处理
|
||||
@IgnoredKey
|
||||
var rawData: Any?
|
||||
|
||||
/// 是否请求成功(基于业务状态码)
|
||||
var isSuccess: Bool {
|
||||
return code == XSNetwork.SuccessCode
|
||||
}
|
||||
}
|
||||
|
||||
/// 分页列表模型
|
||||
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?
|
||||
|
||||
/// SmartCodable 默认支持下划线转驼峰,但显式定义更安全
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case current_page = "current_page"
|
||||
case page_size = "page_size"
|
||||
case page_total = "page_total"
|
||||
case total_size = "total_size"
|
||||
}
|
||||
}
|
||||
}
|
||||
76
XSeri/Base/Networking/Base/XSNetworkMonitorManager.swift
Normal file
76
XSeri/Base/Networking/Base/XSNetworkMonitorManager.swift
Normal file
@ -0,0 +1,76 @@
|
||||
//
|
||||
// XSNetworkMonitorManager.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Network
|
||||
|
||||
class XSNetworkMonitorManager: NSObject {
|
||||
|
||||
static let manager = XSNetworkMonitorManager()
|
||||
|
||||
///是否有网
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
if path.status == .satisfied, self.connectionType != nil {
|
||||
if self.isReachable == false {
|
||||
self.isReachable = true
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil)
|
||||
}
|
||||
} else {
|
||||
self.isReachable = true
|
||||
}
|
||||
} else {
|
||||
if self.isReachable == true {
|
||||
self.isReachable = false
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil)
|
||||
}
|
||||
} else {
|
||||
self.isReachable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
monitor.start(queue: queue)
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
monitor.cancel()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSNetworkMonitorManager {
|
||||
///网络发生变化
|
||||
static let networkStatusDidChangeNotification = Notification.Name(rawValue: "XSNetworkMonitorManager.networkStatusDidChangeNotification")
|
||||
}
|
||||
|
||||
42
XSeri/Base/Networking/Base/XSNetworkPlugin.swift
Normal file
42
XSeri/Base/Networking/Base/XSNetworkPlugin.swift
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// XSNetworkPlugin.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Moya
|
||||
|
||||
/// 网络请求加载指示器插件
|
||||
/// 用于处理请求过程中的 HUD 显示与隐藏,实现 UI 与网络逻辑解耦
|
||||
struct XSNetworkHUDPlugin: PluginType {
|
||||
|
||||
func willSend(_ request: RequestType, target: TargetType) {
|
||||
guard let target = target as? XSNetwork.Target,
|
||||
case let .request(parameters) = target else {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果配置了需要 loading,则在主线程显示 HUD
|
||||
if parameters.isLoding {
|
||||
DispatchQueue.main.async {
|
||||
XSHud.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
|
||||
guard let target = target as? XSNetwork.Target,
|
||||
case let .request(parameters) = target else {
|
||||
return
|
||||
}
|
||||
|
||||
// 无论请求成功或失败,只要配置了 loading,均在结束时隐藏 HUD
|
||||
if parameters.isLoding {
|
||||
DispatchQueue.main.async {
|
||||
XSHud.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
XSeri/Base/Networking/Base/XSNetworkTarget.swift
Normal file
89
XSeri/Base/Networking/Base/XSNetworkTarget.swift
Normal file
@ -0,0 +1,89 @@
|
||||
//
|
||||
// XSNetworkTarget.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import SmartCodable
|
||||
import AdSupport
|
||||
import YYCategories
|
||||
|
||||
extension XSNetwork {
|
||||
enum Target {
|
||||
case request(parameters: XSNetwork.Parameters)
|
||||
}
|
||||
}
|
||||
|
||||
extension XSNetwork.Target: TargetType {
|
||||
var baseURL: URL {
|
||||
return .init(string: XSBaseURL)!
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .request(let parameters):
|
||||
return XSURLPathPrefix + parameters.path
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .request(let parameters):
|
||||
return parameters.method
|
||||
}
|
||||
}
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .request(let parameters):
|
||||
let params = parameters.parameters ?? [:]
|
||||
return .requestParameters(parameters: params, encoding: getEncoding())
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [String : String]? {
|
||||
let userToken = XSLoginManager.manager.token?.token ?? ""
|
||||
let dic: [String : String] = [
|
||||
"system-version" : kXSSystemVersion,
|
||||
"lang-key" : "en", // 当前语言,后续可根据应用语言环境动态获取
|
||||
"time-zone" : String.timeZone(), // 时区
|
||||
"app-version" : kXSVersion,
|
||||
"device-id" : XSDeviceId.shared.id, // 设备id
|
||||
"brand" : "apple", // 品牌
|
||||
"app-name" : kXSBundleName,
|
||||
"system-type" : "ios",
|
||||
"idfa" : ASIdentifierManager.shared().advertisingIdentifier.uuidString,
|
||||
"model" : UIDevice.current.machineModelName ?? "",
|
||||
"authorization" : userToken,
|
||||
"device-gaid" : UIDevice.current.identifierForVendor?.uuidString ?? "",
|
||||
"product-prefix" : "XSeri"
|
||||
]
|
||||
return dic
|
||||
}
|
||||
|
||||
var sampleData: Data {
|
||||
return "".data(using: .utf8)!
|
||||
}
|
||||
}
|
||||
|
||||
private extension XSNetwork.Target {
|
||||
func getEncoding() -> ParameterEncoding {
|
||||
switch self.method {
|
||||
case .get, .delete:
|
||||
return URLEncoding.default
|
||||
default:
|
||||
return JSONEncoding.default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
static func timeZone() -> String {
|
||||
let timeZone = NSTimeZone.local as NSTimeZone
|
||||
let seconds = timeZone.secondsFromGMT / 3600
|
||||
let sign = seconds >= 0 ? "+" : "-"
|
||||
return String(format: "GMT%@%02d:00", sign, abs(seconds))
|
||||
}
|
||||
}
|
||||
25
XSeri/Base/Networking/Base/XSURLPath.swift
Normal file
25
XSeri/Base/Networking/Base/XSURLPath.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// XSURLPath.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
let XSBaseURL = "https://api-breeltv.breeltv.com"
|
||||
let XSURLPathPrefix = "/reel"
|
||||
let XSWebBaseURL = "https://www.breeltv.com"
|
||||
let XSCampaignWebURL = "https://campaign.breeltv.com"
|
||||
|
||||
///用户协议
|
||||
let kXSUserAgreementWebUrl = XSWebBaseURL + "/user_policy"
|
||||
///隐私协议
|
||||
let kXSPrivacyPolicyWebUrl = XSWebBaseURL + "/private"
|
||||
|
||||
///反馈首页
|
||||
let kXSFeedBackHomeWebUrl = XSCampaignWebURL + "/pages/leave/index"
|
||||
///反馈列表
|
||||
let kXSFeedBackListWebUrl = XSCampaignWebURL + "/pages/leave/list"
|
||||
///反馈详情
|
||||
let kXSFeedBackDetailWebUrl = XSCampaignWebURL + "/pages/leave/detail"
|
||||
///注销账号
|
||||
let kXSLogoutWebUrl = XSCampaignWebURL + "/pages/setting/logout"
|
||||
56
XSeri/Base/View/XSButton.swift
Normal file
56
XSeri/Base/View/XSButton.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// XSButton.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XSButton: UIButton {
|
||||
|
||||
override class var layerClass: AnyClass {
|
||||
return CAGradientLayer.self
|
||||
}
|
||||
|
||||
var xs_gradientLayer: CAGradientLayer {
|
||||
return self.layer as! CAGradientLayer
|
||||
}
|
||||
|
||||
var xs_colors: [CGColor]? {
|
||||
get {
|
||||
return xs_gradientLayer.colors as? [CGColor]
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.colors = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var xs_startPoint: CGPoint {
|
||||
get {
|
||||
return xs_gradientLayer.startPoint
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.startPoint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var xs_endPoint: CGPoint {
|
||||
get {
|
||||
return xs_gradientLayer.endPoint
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.endPoint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var xs_locations: [NSNumber]? {
|
||||
get {
|
||||
return xs_gradientLayer.locations
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.locations = newValue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
23
XSeri/Base/View/XSCollectionView.swift
Normal file
23
XSeri/Base/View/XSCollectionView.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// XSCollectionView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XSCollectionView: 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")
|
||||
}
|
||||
|
||||
}
|
||||
20
XSeri/Base/View/XSCustomTabBar.swift
Normal file
20
XSeri/Base/View/XSCustomTabBar.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// XSCustomTabBar.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/01/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import ESTabBarController_swift
|
||||
|
||||
final class XSCustomTabBar: ESTabBar {
|
||||
|
||||
var contentHeight: CGFloat = 60
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
var result = super.sizeThatFits(size)
|
||||
result.height = contentHeight + XSScreen.safeBottom
|
||||
return result
|
||||
}
|
||||
}
|
||||
98
XSeri/Base/View/XSImageView.swift
Normal file
98
XSeri/Base/View/XSImageView.swift
Normal file
@ -0,0 +1,98 @@
|
||||
//
|
||||
// XSImageView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
class XSImageView: UIImageView {
|
||||
|
||||
var placeholderColor = UIColor.black
|
||||
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)
|
||||
xs_init()
|
||||
}
|
||||
|
||||
override init(image: UIImage?) {
|
||||
super.init(image: image)
|
||||
xs_init()
|
||||
}
|
||||
|
||||
override init(image: UIImage?, highlightedImage: UIImage?) {
|
||||
super.init(image: image, highlightedImage: highlightedImage)
|
||||
xs_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
xs_init()
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
xs_init()
|
||||
}
|
||||
|
||||
func xs_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, height: self.bounds.height)
|
||||
placeholderImageView.center = .init(x: self.bounds.width / 2, y: self.bounds.height / 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UIImageView {
|
||||
func xs_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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
XSeri/Base/View/XSLabel.swift
Normal file
56
XSeri/Base/View/XSLabel.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// XSLabel.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XSLabel: UILabel {
|
||||
|
||||
var textColors: [CGColor]?
|
||||
var textStartPoint: CGPoint?
|
||||
var textEndPoint: CGPoint?
|
||||
|
||||
var colorImage: UIImage?
|
||||
|
||||
|
||||
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.xs_getGradientImage(size: size, colors: colors, startPoint: startPoint, endPoint: endPoine))
|
||||
} else if let image = self.colorImage {
|
||||
self.textColor = UIColor(patternImage: image.xs_resized(to: size))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
|
||||
static func xs_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 xs_resized(to size: CGSize) -> UIImage {
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
return renderer.image { _ in
|
||||
self.draw(in: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
76
XSeri/Base/View/XSPanModalContentView.swift
Normal file
76
XSeri/Base/View/XSPanModalContentView.swift
Normal file
@ -0,0 +1,76 @@
|
||||
//
|
||||
// XSPanModalContentView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import HWPanModal
|
||||
|
||||
class XSPanModalContentView: HWPanModalContentView {
|
||||
|
||||
var contentHeight = XSScreen.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.6
|
||||
return config
|
||||
}
|
||||
|
||||
override func allowsTapBackgroundToDismiss() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func allowsDragToDismiss() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func allowsPullDownWhenShortState() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func showsScrollableVerticalScrollIndicator() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func springDamping() -> CGFloat {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func cornerRadius() -> CGFloat {
|
||||
return 18
|
||||
}
|
||||
|
||||
}
|
||||
244
XSeri/Base/View/XSProgressView.swift
Normal file
244
XSeri/Base/View/XSProgressView.swift
Normal file
@ -0,0 +1,244 @@
|
||||
//
|
||||
// XSProgressView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import YYCategories
|
||||
import YYText
|
||||
|
||||
class XSProgressView: 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 = .FFDAA_4.withAlphaComponent(0.39) {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
var currentProgress: UIColor = .FFDAA_4 {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
var lineWidth: CGFloat = 2 {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
///加载中状态
|
||||
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? {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
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: XSScreen.width, height: lineWidth + insets.top + insets.bottom)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
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 XSProgressView {
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
83
XSeri/Base/View/XSTabBarItemContentView.swift
Normal file
83
XSeri/Base/View/XSTabBarItemContentView.swift
Normal file
@ -0,0 +1,83 @@
|
||||
import UIKit
|
||||
import ESTabBarController_swift
|
||||
|
||||
class XSTabBarItemContentView: ESTabBarItemContentView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
// 设置未选中状态的颜色
|
||||
textColor = XSConfig.Color.tabBarInactive
|
||||
iconColor = XSConfig.Color.tabBarInactive
|
||||
|
||||
// 设置选中状态的颜色
|
||||
highlightTextColor = XSConfig.Color.tabBarActive
|
||||
highlightIconColor = XSConfig.Color.tabBarActive
|
||||
|
||||
// 允许标题不压缩,解决省略号问题
|
||||
titleLabel.numberOfLines = 1
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
// 禁止 ImageView 拉伸
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func updateLayout() {
|
||||
super.updateLayout()
|
||||
|
||||
// 1. 处理图标大小 (根据 Figma,选中 36pt,未选中 24pt)
|
||||
let iconSize: CGFloat = selected ? 36.0 : 24.0
|
||||
// 调整纵向位置,确保视觉居中
|
||||
let iconY: CGFloat = selected ? 6.0 : 10.0
|
||||
imageView.frame = CGRect(x: (bounds.size.width - iconSize) / 2.0,
|
||||
y: iconY,
|
||||
width: iconSize,
|
||||
height: iconSize)
|
||||
|
||||
// 2. 处理文字布局,防止省略号
|
||||
titleLabel.font = selected ? XSConfig.Font.tabBarActive : XSConfig.Font.tabBarInactive
|
||||
titleLabel.sizeToFit()
|
||||
// 给予足够的宽度以防被截断
|
||||
let labelWidth = bounds.size.width + 20
|
||||
titleLabel.frame = CGRect(x: (bounds.size.width - labelWidth) / 2.0,
|
||||
y: imageView.frame.maxY + 2.0,
|
||||
width: labelWidth,
|
||||
height: 14.0)
|
||||
}
|
||||
|
||||
override func selectAnimation(animated: Bool, completion: (() -> Void)?) {
|
||||
super.selectAnimation(animated: animated, completion: completion)
|
||||
|
||||
if animated {
|
||||
// 执行平滑放大动画
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut], animations: {
|
||||
self.updateLayout()
|
||||
}, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
self.updateLayout()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
override func deselectAnimation(animated: Bool, completion: (() -> Void)?) {
|
||||
super.deselectAnimation(animated: animated, completion: completion)
|
||||
|
||||
if animated {
|
||||
// 执行平滑缩小还原动画
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn], animations: {
|
||||
self.updateLayout()
|
||||
}, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
self.updateLayout()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
49
XSeri/Base/View/XSTableView.swift
Normal file
49
XSeri/Base/View/XSTableView.swift
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// XSTableView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XSTableView: UITableView {
|
||||
|
||||
var insetGroupedMargins: CGFloat = 15
|
||||
|
||||
override init(frame: CGRect, style: UITableView.Style) {
|
||||
super.init(frame: frame, style: style)
|
||||
separatorColor = .white.withAlphaComponent(0.1)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
49
XSeri/Base/View/XSTableViewCell.swift
Normal file
49
XSeri/Base/View/XSTableViewCell.swift
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// XSTableViewCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XSTableViewCell: UITableViewCell {
|
||||
|
||||
private(set) lazy var xs_indicatorImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "arrow_right_icon_01"))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
56
XSeri/Base/View/XSView.swift
Normal file
56
XSeri/Base/View/XSView.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// XSView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XSView: UIView {
|
||||
|
||||
override class var layerClass: AnyClass {
|
||||
return CAGradientLayer.self
|
||||
}
|
||||
|
||||
var xs_gradientLayer: CAGradientLayer {
|
||||
return self.layer as! CAGradientLayer
|
||||
}
|
||||
|
||||
var xs_colors: [CGColor]? {
|
||||
get {
|
||||
return xs_gradientLayer.colors as? [CGColor]
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.colors = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var xs_startPoint: CGPoint {
|
||||
get {
|
||||
return xs_gradientLayer.startPoint
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.startPoint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var xs_endPoint: CGPoint {
|
||||
get {
|
||||
return xs_gradientLayer.endPoint
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.endPoint = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var xs_locations: [NSNumber]? {
|
||||
get {
|
||||
return xs_gradientLayer.locations
|
||||
}
|
||||
set {
|
||||
xs_gradientLayer.locations = newValue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
78
XSeri/Base/WebView/XSAppWebViewController.swift
Normal file
78
XSeri/Base/WebView/XSAppWebViewController.swift
Normal file
@ -0,0 +1,78 @@
|
||||
//
|
||||
// XSAppWebViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class XSAppWebViewController: XSBaseWebViewController {
|
||||
|
||||
var id: String?
|
||||
|
||||
private var receiveDataCount = 0
|
||||
|
||||
var theme: String? = "theme_3"
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.autoTitle = false
|
||||
|
||||
if webUrl == kXSFeedBackListWebUrl {
|
||||
self.title = "feedback_history".localized
|
||||
} else if webUrl == kXSFeedBackHomeWebUrl {
|
||||
self.title = "feedback".localized
|
||||
} else if webUrl == kXSFeedBackDetailWebUrl {
|
||||
self.title = "feedback_detail".localized
|
||||
} else if webUrl == kXSLogoutWebUrl {
|
||||
self.title = "account_deletion".localized
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func xs_webViewDidFinishLoad(_ webView: XSWebView) {
|
||||
super.xs_webViewDidFinishLoad(webView)
|
||||
receiveDataCount = 0
|
||||
receiveDataFromNative()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSAppWebViewController {
|
||||
|
||||
func receiveDataFromNative() {
|
||||
receiveDataCount += 1
|
||||
if receiveDataCount > 10 { return }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
var dic = [
|
||||
"token" : XSLoginManager.manager.token?.token ?? "",
|
||||
"time_zone" : String.timeZone(),
|
||||
"lang" : "en",
|
||||
"type" : "ios",
|
||||
"device-id" : XSDeviceId.shared.id
|
||||
]
|
||||
|
||||
if let theme = theme {
|
||||
dic["theme"] = theme
|
||||
}
|
||||
|
||||
if let id = id {
|
||||
dic["id"] = id
|
||||
}
|
||||
|
||||
if let json = dic.toJsonString() {
|
||||
let js = "receiveDataFromNative(\(json))"
|
||||
self.webView.evaluateJavaScript(js) { [weak self] _, error in
|
||||
guard let self = self else { return }
|
||||
if error != nil {
|
||||
self.receiveDataFromNative()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
111
XSeri/Base/WebView/XSBaseWebViewController+Script.swift
Normal file
111
XSeri/Base/WebView/XSBaseWebViewController+Script.swift
Normal file
@ -0,0 +1,111 @@
|
||||
//
|
||||
// XSBaseWebViewController+Script.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
import ZLPhotoBrowser
|
||||
import SmartCodable
|
||||
|
||||
|
||||
///APP交互
|
||||
let kXSWebMessageAPP = "js2app"
|
||||
///打开反馈列表
|
||||
let kXSWebMessageOpenFeedbackList = "openFeedbackList"
|
||||
///打开反馈详情
|
||||
let kXSWebMessageOpenFeedbackDetail = "openFeedbackDetail"
|
||||
///打开相册
|
||||
let kXSWebMessageOpenPhotoPicker = "openPhotoPicker"
|
||||
///删除账号成功
|
||||
let kXSWebMessageAccountDeletionFinish = "accountLogout"
|
||||
|
||||
extension XSBaseWebViewController {
|
||||
|
||||
func xs_webViewUserContentController(didReceive message: WKScriptMessage) {
|
||||
let name = message.name
|
||||
let body = message.body
|
||||
|
||||
switch name {
|
||||
case kXSWebMessageOpenFeedbackList:
|
||||
let vc = XSAppWebViewController()
|
||||
vc.webUrl = kXSFeedBackListWebUrl
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
case kXSWebMessageOpenFeedbackDetail:
|
||||
guard let body = body as? [String : Any] else { return }
|
||||
guard let id = body["id"] as? Int else { return }
|
||||
|
||||
let vc = XSAppWebViewController()
|
||||
vc.id = "\(id)"
|
||||
vc.webUrl = kXSFeedBackDetailWebUrl
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
case kXSWebMessageOpenPhotoPicker:
|
||||
openPhotoPicker()
|
||||
|
||||
case kXSWebMessageAPP:
|
||||
guard let body = message.body as? [String : Any] else { return }
|
||||
guard let model = XSWebMessageModel.deserialize(from: body) else { return }
|
||||
let type = model.type
|
||||
let data = model.data
|
||||
|
||||
if type == "login" {
|
||||
// let view = FALoginView()
|
||||
// view.present(in: nil)
|
||||
|
||||
} else if type == "open_notify" {
|
||||
// openNotify()
|
||||
|
||||
} else if type == "watch_video" {
|
||||
let vc = XSShortDetailViewController()
|
||||
vc.shortId = data?.short_play_id
|
||||
vc.activityId = data?.activity_id
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
} else {
|
||||
|
||||
guard let urlStr = data?.link else { return }
|
||||
guard let url = URL(string: urlStr) else { return }
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
case kXSWebMessageAccountDeletionFinish:
|
||||
self.navigationController?.popToRootViewController(animated: true)
|
||||
NotificationCenter.default.post(name: XSLoginManager.loginStateDidChangeNotification, object: nil)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
///打开相册
|
||||
private func openPhotoPicker() {
|
||||
|
||||
ZLPhotoConfiguration.default().allowSelectOriginal = false
|
||||
ZLPhotoConfiguration.default().maxSelectCount = 1
|
||||
ZLPhotoConfiguration.default().allowEditImage = false
|
||||
ZLPhotoConfiguration.default().allowSelectVideo = false
|
||||
ZLPhotoConfiguration.default().allowSelectGif = false
|
||||
ZLPhotoConfiguration.default().allowTakePhotoInLibrary = false
|
||||
|
||||
let picker = ZLPhotoPicker()
|
||||
picker.selectImageBlock = { [weak self] (results, _) in
|
||||
guard let self = self else { return }
|
||||
guard let image = results.first?.image else { return }
|
||||
guard let imageData = image.jpegData(compressionQuality: 0.8) else { return }
|
||||
let imageDataStr = imageData.base64EncodedString(options: .endLineWithCarriageReturn)
|
||||
|
||||
let js = "uploadConvertImage('\(imageDataStr)')"
|
||||
self.webView.evaluateJavaScript(js)
|
||||
}
|
||||
|
||||
picker.showPhotoLibrary(sender: self)
|
||||
}
|
||||
|
||||
}
|
||||
113
XSeri/Base/WebView/XSBaseWebViewController.swift
Normal file
113
XSeri/Base/WebView/XSBaseWebViewController.swift
Normal file
@ -0,0 +1,113 @@
|
||||
//
|
||||
// XSBaseWebViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
import SnapKit
|
||||
|
||||
class XSBaseWebViewController: XSViewController {
|
||||
|
||||
var webUrl: String?
|
||||
|
||||
///自动设置标题
|
||||
var autoTitle = true
|
||||
|
||||
var needAutoRefresh = true
|
||||
|
||||
private(set) lazy var webView: XSWebView = {
|
||||
let controller = WKUserContentController()
|
||||
|
||||
let config = WKWebViewConfiguration()
|
||||
config.userContentController = controller
|
||||
config.defaultWebpagePreferences.allowsContentJavaScript = true
|
||||
config.preferences.javaScriptCanOpenWindowsAutomatically = false
|
||||
let webView = XSWebView(frame: self.view.bounds, configuration: config)
|
||||
webView.delegate = self
|
||||
return webView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// self.edgesForExtendedLayout = []
|
||||
|
||||
xs_setupUI()
|
||||
|
||||
if let url = webUrl {
|
||||
self.load(webUrl: url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||
self.xs_setNavigationStyle()
|
||||
}
|
||||
|
||||
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 XSBaseWebViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
self.view.addSubview(webView)
|
||||
|
||||
self.webView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(0)
|
||||
make.right.equalToSuperview().offset(0)
|
||||
make.bottom.equalToSuperview().offset(0)
|
||||
make.top.equalTo(self.view.safeAreaLayoutGuide)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//MARK: -------------- VPWebViewDelegate --------------
|
||||
extension XSBaseWebViewController: XSWebViewDelegate {
|
||||
|
||||
func xs_webView(_ webView: XSWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool {
|
||||
self.webView.isHidden = false
|
||||
return true
|
||||
}
|
||||
|
||||
func xs_webViewDidStartLoad(_ webView: XSWebView) {
|
||||
XSHud.show(containerView: self.view)
|
||||
}
|
||||
|
||||
func xs_webView(webView: XSWebView, didChangeTitle title: String) {
|
||||
if autoTitle {
|
||||
self.title = title
|
||||
}
|
||||
}
|
||||
|
||||
func xs_webViewDidFinishLoad(_ webView: XSWebView) {
|
||||
self.webView.isHidden = false
|
||||
XSHud.dismiss()
|
||||
}
|
||||
|
||||
func xs_webView(_ webView: XSWebView, didFailLoadWithError error: any Error) {
|
||||
XSHud.dismiss()
|
||||
}
|
||||
|
||||
func xs_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
self.xs_webViewUserContentController(didReceive: message)
|
||||
}
|
||||
|
||||
}
|
||||
22
XSeri/Base/WebView/XSWebMessageModel.swift
Normal file
22
XSeri/Base/WebView/XSWebMessageModel.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// XSWebMessageModel.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SmartCodable
|
||||
|
||||
struct XSWebMessageModel: SmartCodable {
|
||||
var type: String?
|
||||
|
||||
var data: XSWebMessageData?
|
||||
}
|
||||
|
||||
struct XSWebMessageData: SmartCodable {
|
||||
|
||||
var activity_id: String?
|
||||
var short_play_id: String?
|
||||
var link: String?
|
||||
}
|
||||
153
XSeri/Base/WebView/XSWebView.swift
Normal file
153
XSeri/Base/WebView/XSWebView.swift
Normal file
@ -0,0 +1,153 @@
|
||||
//
|
||||
// XSWebView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
import YYText
|
||||
|
||||
//MARK:-------------- VPWebViewDelegate --------------
|
||||
@objc protocol XSWebViewDelegate: NSObjectProtocol {
|
||||
|
||||
@objc optional func xs_webView(_ webView: XSWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool
|
||||
|
||||
@objc optional func xs_webViewDidStartLoad(_ webView: XSWebView)
|
||||
|
||||
@objc optional func xs_webViewDidFinishLoad(_ webView: XSWebView)
|
||||
|
||||
@objc optional func xs_webView(_ webView: XSWebView, didFailLoadWithError error: Error)
|
||||
|
||||
///进度
|
||||
@objc optional func xs_webView(webView: XSWebView, didChangeProgress progress: CGFloat)
|
||||
///标题
|
||||
@objc optional func xs_webView(webView: XSWebView, didChangeTitle title: String)
|
||||
|
||||
///web交互用
|
||||
@objc optional func xs_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
|
||||
|
||||
}
|
||||
|
||||
class XSWebView: WKWebView {
|
||||
weak var delegate: XSWebViewDelegate?
|
||||
|
||||
private(set) var scriptMessageHandlerArray: [String] = [
|
||||
kXSWebMessageAPP,
|
||||
kXSWebMessageOpenFeedbackList,
|
||||
kXSWebMessageOpenFeedbackDetail,
|
||||
kXSWebMessageOpenPhotoPicker,
|
||||
kXSWebMessageAccountDeletionFinish,
|
||||
]
|
||||
|
||||
|
||||
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? XSWebView == self {
|
||||
if keyPath == "estimatedProgress", let progress = change?[NSKeyValueChangeKey.newKey] as? CGFloat {
|
||||
self.delegate?.xs_webView?(webView: self, didChangeProgress: progress)
|
||||
} else if keyPath == "title", let title = change?[NSKeyValueChangeKey.newKey] as? String {
|
||||
self.delegate?.xs_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 XSWebView: 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?.xs_webView?(self, shouldStartLoadWith: navigationAction) {
|
||||
if result {
|
||||
decisionHandler(.allow)
|
||||
} else {
|
||||
decisionHandler(.cancel)
|
||||
}
|
||||
} else {
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
self.delegate?.xs_webViewDidStartLoad?(self)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
self.delegate?.xs_webViewDidFinishLoad?(self)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
self.delegate?.xs_webView?(self, didFailLoadWithError: error)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
self.delegate?.xs_webView?(self, didFailLoadWithError: error)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//MARK:-------------- WKScriptMessageHandler --------------
|
||||
extension XSWebView: WKScriptMessageHandler {
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
self.delegate?.xs_userContentController?(userContentController, didReceive: message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
//
|
||||
// XSDiscoverViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import JXPlayer
|
||||
|
||||
class XSDiscoverViewController: JXPlayerListViewController {
|
||||
|
||||
override var contentSize: CGSize {
|
||||
return .init(width: XSScreen.width, height: XSScreen.height)
|
||||
}
|
||||
|
||||
override var ViewModelClass: JXPlayerListViewModel.Type {
|
||||
return XSDiscoverViewModel.self
|
||||
}
|
||||
|
||||
var xs_viewModel: XSDiscoverViewModel {
|
||||
return self.viewModel as! XSDiscoverViewModel
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.register(XSDiscoverPlayerCell.self, forCellWithReuseIdentifier: "cell")
|
||||
self.delegate = self
|
||||
self.dataSource = self
|
||||
|
||||
Task {
|
||||
await self.xs_viewModel.requestDataArr(page: 1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
self.viewModel.currentCell?.pause()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//MARK: JXPlayerListViewControllerDataSource
|
||||
extension XSDiscoverViewController: JXPlayerListViewControllerDataSource {
|
||||
func jx_playerListViewController(_ viewController: JXPlayerListViewController, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = self.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSDiscoverPlayerCell
|
||||
cell.model = xs_viewModel.dataArr[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func jx_playerListViewController(_ viewController: JXPlayerListViewController, numberOfItemsInSection section: Int) -> Int {
|
||||
return xs_viewModel.dataArr.count
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: JXPlayerListViewControllerDelegate
|
||||
extension XSDiscoverViewController: JXPlayerListViewControllerDelegate {
|
||||
func jx_playerViewControllerLoadMoreData(playerViewController: JXPlayerListViewController) {
|
||||
Task {
|
||||
await self.xs_viewModel.requestDataArr(page: self.xs_viewModel.page + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
201
XSeri/Class/Discover/View/XSDiscoverControlView.swift
Normal file
201
XSeri/Class/Discover/View/XSDiscoverControlView.swift
Normal file
@ -0,0 +1,201 @@
|
||||
//
|
||||
// XSDiscoverControlView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import JXPlayer
|
||||
import SnapKit
|
||||
|
||||
class XSDiscoverControlView: JXPlayerListControlView {
|
||||
|
||||
|
||||
override var model: Any? {
|
||||
didSet {
|
||||
let model = self.model as? XSShortModel
|
||||
let videoInfo = model?.video_info
|
||||
|
||||
epView.currentEp = videoInfo?.episode ?? ""
|
||||
epView.totalEp = "\(model?.episode_total ?? 0)"
|
||||
|
||||
collectButton.setNeedsUpdateConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
override var viewModel: JXPlayerListViewModel? {
|
||||
didSet {
|
||||
self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override var durationTime: TimeInterval {
|
||||
didSet {
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
|
||||
override var currentTime: TimeInterval {
|
||||
didSet {
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
|
||||
override var isCurrent: Bool {
|
||||
didSet {
|
||||
updatePlayIconState()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var progressView: XSProgressView = {
|
||||
let view = XSProgressView()
|
||||
view.insets = .init(top: 10, left: 16, bottom: 10, right: 16)
|
||||
view.panFinish = { [weak self] progress in
|
||||
guard let self = self else { return }
|
||||
self.viewModel?.seekTo(Float(progress))
|
||||
}
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var epView: XSPlayerEpButton = {
|
||||
let view = XSPlayerEpButton()
|
||||
view.addAction(UIAction(handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let vc = XSShortDetailViewController()
|
||||
vc.shortId = (self.model as? XSShortModel)?.short_play_id
|
||||
self.viewController?.navigationController?.pushViewController(vc, animated: true)
|
||||
}), for: .touchUpInside)
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var collectButton: UIButton = {
|
||||
var configuration = UIButton.Configuration.plain()
|
||||
configuration.contentInsets = .zero
|
||||
configuration.imagePadding = 2
|
||||
configuration.imagePlacement = .top
|
||||
configuration.attributedTitle = AttributedString("Save".localized, attributes: AttributeContainer([
|
||||
.font : UIFont.font(ofSize: 12, weight: .bold),
|
||||
.foregroundColor : UIColor.FFDAA_4
|
||||
]))
|
||||
|
||||
let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard let model = self.model as? XSShortModel else { return }
|
||||
let isCollect = !(model.is_collect ?? false)
|
||||
|
||||
Task {
|
||||
await XSVideoAPI.requestCollectShort(isCollect: isCollect, shortId: model.short_play_id ?? "0", videoId: model.video_info?.short_play_video_id ?? "0")
|
||||
}
|
||||
}))
|
||||
button.configurationUpdateHandler = { [weak self] button in
|
||||
guard let self = self else { return }
|
||||
let model = self.model as? XSShortModel
|
||||
let videoInfo = model?.video_info
|
||||
|
||||
if model?.is_collect == true {
|
||||
button.configuration?.image = UIImage(named: "collect_icon_01_selected")
|
||||
} else {
|
||||
button.configuration?.image = UIImage(named: "collect_icon_01")
|
||||
}
|
||||
}
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var playIconImageView = UIImageView()
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: XSVideoAPI.updateShortCollectStateNotification, object: nil)
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
@MainActor 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 == "isPlaying" {
|
||||
self.updatePlayIconState()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
if durationTime == 0 || currentTime == 0 {
|
||||
progressView.progress = 0
|
||||
return
|
||||
}
|
||||
progressView.progress = currentTime / durationTime
|
||||
}
|
||||
|
||||
override func singleTapEvent() {
|
||||
super.singleTapEvent()
|
||||
self.viewModel?.userSwitchPlayAndPause()
|
||||
}
|
||||
|
||||
private func updatePlayIconState() {
|
||||
let model = self.model as? XSShortModel
|
||||
let videoInfo = model?.video_info
|
||||
|
||||
if videoInfo?.is_lock == true {
|
||||
playIconImageView.isHidden = true
|
||||
} else {
|
||||
playIconImageView.isHidden = false
|
||||
if isCurrent == true, self.viewModel?.isPlaying != true {
|
||||
playIconImageView.image = UIImage(named: "play_icon_01")
|
||||
} else {
|
||||
playIconImageView.image = UIImage(named: "pause_icon_01")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func updateShortCollectStateNotification(sender: Notification) {
|
||||
let model = self.model as? XSShortModel
|
||||
|
||||
guard let userInfo = sender.userInfo else { return }
|
||||
guard let shortPlayId = userInfo["id"] as? String else { return }
|
||||
guard let state = userInfo["state"] as? Bool else { return }
|
||||
guard shortPlayId == model?.short_play_id else { return }
|
||||
model?.is_collect = state
|
||||
self.collectButton.setNeedsUpdateConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
extension XSDiscoverControlView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(progressView)
|
||||
addSubview(epView)
|
||||
addSubview(collectButton)
|
||||
addSubview(playIconImageView)
|
||||
|
||||
progressView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.right.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-(XSScreen.customTabBarHeight))
|
||||
}
|
||||
|
||||
epView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalTo(progressView.snp.top).offset(-2)
|
||||
}
|
||||
|
||||
collectButton.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(epView.snp.top).offset(-18)
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
}
|
||||
|
||||
playIconImageView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
XSeri/Class/Discover/View/XSDiscoverPlayerCell.swift
Normal file
28
XSeri/Class/Discover/View/XSDiscoverPlayerCell.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// XSDiscoverPlayerCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import JXPlayer
|
||||
|
||||
class XSDiscoverPlayerCell: JXPlayerListCell {
|
||||
|
||||
|
||||
override var ControlViewClass: JXPlayerListControlView.Type {
|
||||
return XSDiscoverControlView.self
|
||||
}
|
||||
|
||||
|
||||
override var model: Any? {
|
||||
didSet {
|
||||
let model = self.model as? XSShortModel
|
||||
let videoInfo = model?.video_info
|
||||
|
||||
self.player.setPlayUrl(url: videoInfo?.video_url ?? "")
|
||||
self.player.coverImageView?.xs_setImage(model?.image_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
54
XSeri/Class/Discover/ViewModel/XSDiscoverViewModel.swift
Normal file
54
XSeri/Class/Discover/ViewModel/XSDiscoverViewModel.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// XSDiscoverViewModel.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import JXPlayer
|
||||
|
||||
class XSDiscoverViewModel: JXPlayerListViewModel {
|
||||
|
||||
private(set) var dataArr: [XSShortModel] = []
|
||||
private(set) var page = 1
|
||||
|
||||
@MainActor
|
||||
private func addDataArr(dataArr: [XSShortModel]) {
|
||||
guard dataArr.count > 0 else { return }
|
||||
|
||||
var indexPaths: [IndexPath] = []
|
||||
var startRow = self.dataArr.count
|
||||
|
||||
dataArr.forEach { _ in
|
||||
indexPaths.append(IndexPath(row: startRow, section: 0))
|
||||
startRow += 1
|
||||
}
|
||||
self.dataArr += dataArr
|
||||
|
||||
CATransaction.setCompletionBlock(nil)
|
||||
CATransaction.begin()
|
||||
self.playerListVC?.collectionView.insertItems(at: indexPaths)
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
extension XSDiscoverViewModel {
|
||||
|
||||
@MainActor
|
||||
func requestDataArr(page: Int, completer: (() -> Void)? = nil) async {
|
||||
guard let dataArr = await XSHomeAPI.requestDiscoverData(page: page) else { return }
|
||||
self.page = page
|
||||
if page == 1 {
|
||||
self.playerListVC?.clearData()
|
||||
self.dataArr = dataArr
|
||||
self.playerListVC?.reloadData { [weak self] in
|
||||
|
||||
self?.playerListVC?.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false)
|
||||
}
|
||||
} else {
|
||||
self.addDataArr(dataArr: dataArr)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
133
XSeri/Class/Home/Controller/XSHomeCategoriesViewController.swift
Normal file
133
XSeri/Class/Home/Controller/XSHomeCategoriesViewController.swift
Normal file
@ -0,0 +1,133 @@
|
||||
//
|
||||
// XSHomeCategoriesViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeCategoriesViewController: XSHomeChildViewController {
|
||||
|
||||
|
||||
private lazy var categoryArr: [XSCategoryModel] = []
|
||||
|
||||
private lazy var categoryIndex = 0
|
||||
|
||||
private lazy var dataArr: [XSShortModel] = []
|
||||
|
||||
private lazy var page = 1
|
||||
|
||||
private lazy var layout: UICollectionViewFlowLayout = {
|
||||
let width = (XSScreen.width - 32 - 20) / 3
|
||||
let coverHeight = 142 / 107 * width
|
||||
let height = coverHeight + 40
|
||||
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.itemSize = .init(width: floor(width), height: height)
|
||||
layout.minimumLineSpacing = 10
|
||||
layout.minimumInteritemSpacing = 10
|
||||
layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16)
|
||||
layout.headerReferenceSize = .init(width: XSScreen.width, height: 1)
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: XSCollectionView = {
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.showsHorizontalScrollIndicator = false
|
||||
collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0)
|
||||
collectionView.register(XSHomeCategoriesCell.self, forCellWithReuseIdentifier: "cell")
|
||||
collectionView.register(XSHomeCategoriesHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
xs_setupUI()
|
||||
|
||||
Task {
|
||||
await requestCategories()
|
||||
await requestCategoryData(page: 1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSHomeCategoriesViewController {
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(collectionView)
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(self.topHeight + 15)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSHomeCategoriesViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSHomeCategoriesCell
|
||||
cell.model = self.dataArr[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return self.dataArr.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
||||
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! XSHomeCategoriesHeaderView
|
||||
view.didChangeHeight = { [weak self] height in
|
||||
guard let self = self else { return }
|
||||
self.layout.headerReferenceSize = .init(width: XSScreen.width, height: height)
|
||||
}
|
||||
view.didSelected = { [weak self] index in
|
||||
guard let self = self else { return }
|
||||
self.categoryIndex = index
|
||||
Task {
|
||||
await self.requestCategoryData(page: 1)
|
||||
}
|
||||
}
|
||||
view.dataArr = self.categoryArr
|
||||
return view
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
let model = self.dataArr[indexPath.row]
|
||||
let vc = XSShortDetailViewController()
|
||||
vc.shortId = model.short_play_id
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomeCategoriesViewController {
|
||||
|
||||
private func requestCategories() async {
|
||||
guard let dataArr = await XSHomeAPI.requestCategoryList() else { return }
|
||||
|
||||
self.categoryArr = dataArr
|
||||
self.layout.headerReferenceSize = .init(width: XSScreen.width, height: 1)
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
|
||||
private func requestCategoryData(page: Int) async {
|
||||
guard self.categoryIndex < self.categoryArr.count else { return }
|
||||
guard let id = self.categoryArr[categoryIndex].id else { return }
|
||||
|
||||
guard let dataArr = await XSHomeAPI.requestCategoryVideo(id: id, page: page) else { return }
|
||||
|
||||
if page == 1 {
|
||||
self.dataArr.removeAll()
|
||||
}
|
||||
self.dataArr += dataArr
|
||||
self.page = page
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
24
XSeri/Class/Home/Controller/XSHomeChildViewController.swift
Normal file
24
XSeri/Class/Home/Controller/XSHomeChildViewController.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// XSHomeChildViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeChildViewController: XSViewController {
|
||||
|
||||
var topHeight: CGFloat {
|
||||
return XSScreen.safeTop + 95
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.view.backgroundColor = .clear
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
180
XSeri/Class/Home/Controller/XSHomeNewViewController.swift
Normal file
180
XSeri/Class/Home/Controller/XSHomeNewViewController.swift
Normal file
@ -0,0 +1,180 @@
|
||||
//
|
||||
// XSHomeNewViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeNewViewController: XSHomeChildViewController {
|
||||
|
||||
private lazy var dataArr: [XSShortModel] = []
|
||||
private lazy var page: Int = 1
|
||||
|
||||
|
||||
private lazy var layout: UICollectionViewCompositionalLayout = {
|
||||
let layout = UICollectionViewCompositionalLayout { section, environment in
|
||||
|
||||
|
||||
if section == 0 {
|
||||
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(32)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
|
||||
|
||||
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .absolute(210), heightDimension: .absolute(360)), subitems: [item])
|
||||
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||
layoutSection.interGroupSpacing = 15
|
||||
layoutSection.orthogonalScrollingBehavior = .continuous
|
||||
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
|
||||
layoutSection.boundarySupplementaryItems = [headerItem]
|
||||
return layoutSection
|
||||
} else {
|
||||
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(57)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
|
||||
|
||||
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(146)), subitems: [item])
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||
layoutSection.interGroupSpacing = 12
|
||||
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
|
||||
layoutSection.boundarySupplementaryItems = [headerItem]
|
||||
return layoutSection
|
||||
}
|
||||
}
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: XSCollectionView = {
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0)
|
||||
collectionView.xs_addRefreshHeader { [weak self] in
|
||||
self?.handleHeaderRefresh(nil)
|
||||
}
|
||||
collectionView.xs_addRefreshFooter(insetBottom: 0) { [weak self] in
|
||||
self?.handleFooterRefresh(nil)
|
||||
}
|
||||
collectionView.register(XSHomeNewBigCell.self, forCellWithReuseIdentifier: "XSHomeNewBigCell")
|
||||
collectionView.register(XSHomeNewCell.self, forCellWithReuseIdentifier: "XSHomeNewCell")
|
||||
collectionView.register(XSHomeNewTitleView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "title")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
xs_setupUI()
|
||||
|
||||
Task {
|
||||
await requestDataArr(page: 1)
|
||||
}
|
||||
}
|
||||
|
||||
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||
Task {
|
||||
await requestDataArr(page: 1)
|
||||
self.collectionView.xs_endHeaderRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
override func handleFooterRefresh(_ completer: (() -> Void)?) {
|
||||
Task {
|
||||
await requestDataArr(page: self.page + 1)
|
||||
self.collectionView.xs_endFooterRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension XSHomeNewViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(collectionView)
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(self.topHeight + 13)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSHomeNewViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
var model: XSShortModel
|
||||
if indexPath.section == 0 {
|
||||
model = self.dataArr[indexPath.row]
|
||||
} else {
|
||||
model = self.dataArr[indexPath.row + 3]
|
||||
}
|
||||
|
||||
|
||||
if indexPath.section == 0 {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomeNewBigCell", for: indexPath) as! XSHomeNewBigCell
|
||||
cell.model = model
|
||||
return cell
|
||||
} else {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomeNewCell", for: indexPath) as! XSHomeNewCell
|
||||
cell.model = model
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
let totalCount = self.dataArr.count
|
||||
if section == 0 {
|
||||
return min(3, totalCount)
|
||||
} else {
|
||||
return totalCount - 3
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
if self.dataArr.count > 3 {
|
||||
return 2
|
||||
} else if self.dataArr.count > 0 {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
||||
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "title", for: indexPath) as! XSHomeNewTitleView
|
||||
view.titleLabel.text = "New Releases".localized
|
||||
return view
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
var model: XSShortModel
|
||||
if indexPath.section == 0 {
|
||||
model = self.dataArr[indexPath.row]
|
||||
} else {
|
||||
model = self.dataArr[indexPath.row + 3]
|
||||
}
|
||||
let vc = XSShortDetailViewController()
|
||||
vc.shortId = model.short_play_id
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomeNewViewController {
|
||||
|
||||
private func requestDataArr(page: Int) async {
|
||||
guard let list = await XSHomeAPI.requestHomeNew(page: page) else { return }
|
||||
if page == 1 {
|
||||
self.dataArr.removeAll()
|
||||
}
|
||||
self.page = page
|
||||
self.dataArr += list
|
||||
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
165
XSeri/Class/Home/Controller/XSHomePopularViewController.swift
Normal file
165
XSeri/Class/Home/Controller/XSHomePopularViewController.swift
Normal file
@ -0,0 +1,165 @@
|
||||
//
|
||||
// XSHomePopularViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomePopularViewController: XSHomeChildViewController {
|
||||
|
||||
private lazy var viewModel = XSHomeViewModel()
|
||||
|
||||
private lazy var layout: XSWaterfallFlowLayout = {
|
||||
let layout = XSWaterfallFlowLayout()
|
||||
layout.delegate = self
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: XSCollectionView = {
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: self.layout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0)
|
||||
collectionView.xs_addRefreshHeader { [weak self] in
|
||||
self?.handleHeaderRefresh(nil)
|
||||
}
|
||||
collectionView.register(XSHomePopularCell.self, forCellWithReuseIdentifier: "XSHomePopularCell")
|
||||
collectionView.register(XSHomePopularBigCell.self, forCellWithReuseIdentifier: "XSHomePopularBigCell")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil)
|
||||
|
||||
xs_setupUI()
|
||||
|
||||
Task {
|
||||
await self.requestDataArr()
|
||||
}
|
||||
}
|
||||
|
||||
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||
Task {
|
||||
await self.requestDataArr()
|
||||
self.collectionView.xs_endHeaderRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func networkStatusDidChangeNotification() {
|
||||
guard XSNetworkMonitorManager.manager.isReachable == true else { return }
|
||||
guard self.viewModel.dataArr.isEmpty else { return }
|
||||
Task {
|
||||
await self.requestDataArr()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSHomePopularViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(collectionView)
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(self.topHeight + 15)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSHomePopularViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let itemModel = self.viewModel.dataArr[indexPath.section]
|
||||
let model = itemModel.list?[indexPath.row]
|
||||
|
||||
if itemModel.module_key == .week_highest_recommend {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomePopularCell", for: indexPath) as! XSHomePopularCell
|
||||
cell.model = model
|
||||
return cell
|
||||
} else {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomePopularBigCell", for: indexPath) as! XSHomePopularBigCell
|
||||
cell.model = model
|
||||
return cell
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return self.viewModel.dataArr[section].list?.count ?? 0
|
||||
}
|
||||
|
||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return self.viewModel.dataArr.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
let itemModel = self.viewModel.dataArr[indexPath.section]
|
||||
let model = itemModel.list?[indexPath.row]
|
||||
let vc = XSShortDetailViewController()
|
||||
vc.shortId = model?.short_play_id
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: XSWaterfallMutiSectionDelegate
|
||||
extension XSHomePopularViewController: XSWaterfallMutiSectionDelegate {
|
||||
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
|
||||
if indexPath.section == 0 {
|
||||
return 146 + 57
|
||||
} else {
|
||||
return 219 + 92
|
||||
}
|
||||
}
|
||||
|
||||
func columnNumber(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> Int {
|
||||
if section == 0 {
|
||||
return 3
|
||||
} else {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func insetForSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> UIEdgeInsets {
|
||||
return .init(top: 0, left: 16, bottom: 0, right: 16)
|
||||
}
|
||||
|
||||
func interitemSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||
return 13
|
||||
}
|
||||
|
||||
func lineSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||
return 16
|
||||
}
|
||||
|
||||
func spacingWithLastSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||
if section > 0 {
|
||||
return 26
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomePopularViewController {
|
||||
|
||||
private func requestDataArr() async {
|
||||
await self.viewModel.requestHomeData()
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
111
XSeri/Class/Home/Controller/XSHomeRankingsViewController.swift
Normal file
111
XSeri/Class/Home/Controller/XSHomeRankingsViewController.swift
Normal file
@ -0,0 +1,111 @@
|
||||
//
|
||||
// XSHomeRankingsViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeRankingsViewController: XSHomeChildViewController {
|
||||
|
||||
private lazy var dataArr: [XSShortModel] = []
|
||||
|
||||
private lazy var bgView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "rankings_bg_image"))
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var layout: UICollectionViewLayout = {
|
||||
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
|
||||
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(84)), subitems: [item])
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection(group: group)
|
||||
layoutSection.interGroupSpacing = 10
|
||||
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(section: layoutSection)
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: XSCollectionView = {
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0)
|
||||
collectionView.xs_addRefreshHeader { [weak self] in
|
||||
self?.handleHeaderRefresh(nil)
|
||||
}
|
||||
collectionView.register(XSHomeRankingsCell.self, forCellWithReuseIdentifier: "cell")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
xs_setupUI()
|
||||
|
||||
Task {
|
||||
await requestDataArr()
|
||||
}
|
||||
}
|
||||
|
||||
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||
Task {
|
||||
await requestDataArr()
|
||||
self.collectionView.xs_endHeaderRefreshing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomeRankingsViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(bgView)
|
||||
view.addSubview(collectionView)
|
||||
|
||||
bgView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
}
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(self.topHeight + 15)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSHomeRankingsViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSHomeRankingsCell
|
||||
cell.num = indexPath.row + 1
|
||||
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 = XSShortDetailViewController()
|
||||
vc.shortId = model.short_play_id
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomeRankingsViewController {
|
||||
|
||||
private func requestDataArr() async {
|
||||
guard let list = await XSHomeAPI.requestHomeRankings() else { return }
|
||||
|
||||
self.dataArr = list
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
131
XSeri/Class/Home/Controller/XSHomeViewController.swift
Normal file
131
XSeri/Class/Home/Controller/XSHomeViewController.swift
Normal file
@ -0,0 +1,131 @@
|
||||
//
|
||||
// XSHomeViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
import JXSegmentedView
|
||||
|
||||
class XSHomeViewController: XSViewController {
|
||||
|
||||
private let titles: [String] = [
|
||||
"Popular".localized,
|
||||
"New".localized,
|
||||
"Rankings".localized,
|
||||
"Categories".localized,
|
||||
]
|
||||
|
||||
private let viewControllers: [XSViewController] = {
|
||||
let vc1 = XSHomePopularViewController()
|
||||
let vc2 = XSHomeNewViewController()
|
||||
let vc3 = XSHomeRankingsViewController()
|
||||
let vc4 = XSHomeCategoriesViewController()
|
||||
return [vc1, vc2, vc3, vc4]
|
||||
}()
|
||||
|
||||
// MARK: - UI Components
|
||||
private lazy var searchButton: XSHomeSearchButton = {
|
||||
let button = XSHomeSearchButton()
|
||||
button.addAction(UIAction(handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let vc = XSSearchViewController()
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
}), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
/// Segmented 数据源
|
||||
private lazy var segmentedDataSource: JXSegmentedTitleDataSource = {
|
||||
let dataSource = JXSegmentedTitleDataSource()
|
||||
dataSource.titles = self.titles
|
||||
dataSource.titleNormalColor = UIColor.white.withAlphaComponent(0.55)
|
||||
dataSource.titleSelectedColor = .white
|
||||
dataSource.titleNormalFont = .font(ofSize: 16, weight: .medium)
|
||||
dataSource.titleSelectedFont = .font(ofSize: 16, weight: .semibold)
|
||||
dataSource.isTitleColorGradientEnabled = true
|
||||
dataSource.itemSpacing = 24
|
||||
return dataSource
|
||||
}()
|
||||
|
||||
/// Segmented 视图 (Tab 栏)
|
||||
private lazy var segmentedView: JXSegmentedView = {
|
||||
let view = JXSegmentedView()
|
||||
view.dataSource = segmentedDataSource
|
||||
view.delegate = self
|
||||
view.listContainer = listContainerView
|
||||
view.contentEdgeInsetLeft = 16
|
||||
view.contentEdgeInsetRight = 16
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
/// 列表容器 (内容展示)
|
||||
private lazy var listContainerView: JXSegmentedListContainerView = {
|
||||
return JXSegmentedListContainerView(dataSource: self)
|
||||
}()
|
||||
|
||||
// MARK: - Life Cycle
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI Setup
|
||||
extension XSHomeViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(listContainerView)
|
||||
view.addSubview(searchButton)
|
||||
view.addSubview(segmentedView)
|
||||
|
||||
searchButton.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(XSScreen.safeTop + 15)
|
||||
make.height.equalTo(32)
|
||||
}
|
||||
|
||||
|
||||
segmentedView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.right.equalToSuperview()
|
||||
make.top.equalTo(searchButton.snp.bottom).offset(10)
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
|
||||
|
||||
listContainerView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalToSuperview()
|
||||
// make.top.equalTo(segmentedView.snp.bottom).offset(8)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JXSegmentedViewDelegate
|
||||
extension XSHomeViewController: JXSegmentedViewDelegate {
|
||||
func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) {
|
||||
// Tab 切换回调
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JXSegmentedListContainerViewDataSource
|
||||
extension XSHomeViewController: JXSegmentedListContainerViewDataSource {
|
||||
func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int {
|
||||
return segmentedDataSource.titles.count
|
||||
}
|
||||
|
||||
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate {
|
||||
return viewControllers[index]
|
||||
}
|
||||
}
|
||||
390
XSeri/Class/Home/Controller/XSSearchViewController.swift
Normal file
390
XSeri/Class/Home/Controller/XSSearchViewController.swift
Normal file
@ -0,0 +1,390 @@
|
||||
//
|
||||
// XSSearchViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
import LYEmptyView
|
||||
|
||||
final class XSSearchViewController: XSViewController {
|
||||
|
||||
private enum Mode {
|
||||
case idle
|
||||
case suggest
|
||||
case result
|
||||
}
|
||||
|
||||
private let headerView = XSSearchHeaderView()
|
||||
private let historyHotView = XSSearchHistoryHotView()
|
||||
|
||||
private lazy var suggestionCollectionView: XSCollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.minimumLineSpacing = 12
|
||||
layout.minimumInteritemSpacing = 0
|
||||
layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16)
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.isScrollEnabled = true
|
||||
collectionView.keyboardDismissMode = .onDrag
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.register(XSSearchSuggestionCell.self, forCellWithReuseIdentifier: "XSSearchSuggestionCell")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
private lazy var resultCollectionView: XSCollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.minimumLineSpacing = 16
|
||||
layout.minimumInteritemSpacing = 8
|
||||
layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16)
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.isScrollEnabled = true
|
||||
collectionView.keyboardDismissMode = .onDrag
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.register(XSSearchResultCell.self, forCellWithReuseIdentifier: "XSSearchResultCell")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
private var history: [String] = []
|
||||
private var suggestionItems: [XSShortModel] = []
|
||||
private var resultItems: [XSShortModel] = []
|
||||
private var currentSuggestKeyword = ""
|
||||
private var currentResultKeyword = ""
|
||||
|
||||
private var mode: Mode = .idle {
|
||||
didSet {
|
||||
xs_updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private var suggestionTask: Task<Void, Never>?
|
||||
private var resultTask: Task<Void, Never>?
|
||||
private var hotTask: Task<Void, Never>?
|
||||
|
||||
deinit {
|
||||
suggestionTask?.cancel()
|
||||
resultTask?.cancel()
|
||||
hotTask?.cancel()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
xs_setupUI()
|
||||
xs_bindData()
|
||||
xs_loadHistory()
|
||||
xs_loadHotSearches()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
let _ = self.headerView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSSearchViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(headerView)
|
||||
view.addSubview(historyHotView)
|
||||
view.addSubview(suggestionCollectionView)
|
||||
view.addSubview(resultCollectionView)
|
||||
|
||||
headerView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(XSScreen.safeTop + 12)
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
make.height.equalTo(38)
|
||||
}
|
||||
|
||||
historyHotView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headerView.snp.bottom).offset(22)
|
||||
make.left.right.equalToSuperview()
|
||||
}
|
||||
|
||||
suggestionCollectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headerView.snp.bottom).offset(22)
|
||||
make.left.right.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
|
||||
resultCollectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headerView.snp.bottom).offset(22)
|
||||
make.left.right.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
|
||||
// 搜索结果为空时显示空白样式
|
||||
resultCollectionView.ly_emptyView = XSEmpty.xs_emptyView()
|
||||
// 联想为空时显示空白样式
|
||||
suggestionCollectionView.ly_emptyView = XSEmpty.xs_emptyView()
|
||||
}
|
||||
|
||||
private func xs_bindData() {
|
||||
headerView.placeholder = XSSearchData.searchPlaceholder
|
||||
headerView.didTapBack = { [weak self] in
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
headerView.didBeginEditing = { [weak self] text in
|
||||
guard let self = self else { return }
|
||||
// 已经显示搜索结果时,不在进入编辑时立刻切换联想
|
||||
if self.mode == .result { return }
|
||||
self.xs_handleSuggest(text)
|
||||
}
|
||||
headerView.didChangeText = { [weak self] text in
|
||||
self?.xs_handleSuggest(text)
|
||||
}
|
||||
headerView.didTapSearch = { [weak self] text in
|
||||
self?.xs_submitSearch(text)
|
||||
}
|
||||
|
||||
historyHotView.historyTitle = XSSearchData.recentTitle
|
||||
historyHotView.didTapClear = { [weak self] in
|
||||
self?.xs_clearHistory()
|
||||
}
|
||||
historyHotView.didSelectHistory = { [weak self] text in
|
||||
self?.headerView.text = text
|
||||
self?.xs_submitSearch(text)
|
||||
}
|
||||
historyHotView.didSelectHot = { [weak self] model in
|
||||
self?.xs_pushDetail(model: model)
|
||||
}
|
||||
historyHotView.hotTitle = XSSearchData.hotSearchesTitle
|
||||
xs_updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data
|
||||
extension XSSearchViewController {
|
||||
|
||||
private func xs_loadHotSearches() {
|
||||
hotTask?.cancel()
|
||||
hotTask = Task { [weak self] in
|
||||
// 热门榜单数据来源接口
|
||||
let hotList = await XSHomeAPI.requestHotSearchList() ?? []
|
||||
let topList = await XSHomeAPI.requestTopSearchList() ?? []
|
||||
|
||||
guard !Task.isCancelled else { return }
|
||||
await MainActor.run {
|
||||
self?.xs_updateHotSection(hotList: Array(hotList.prefix(5)), topList: Array(topList.prefix(5)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func xs_updateHotSection(hotList: [XSShortModel], topList: [XSShortModel]) {
|
||||
|
||||
var arr: [XSSearchHotSection] = []
|
||||
|
||||
if !hotList.isEmpty {
|
||||
arr.append(XSSearchHotSection(icon: UIImage(named: "hot_icon_03"), title: XSSearchData.hotSectionTitles[0], style: .gold, items: hotList))
|
||||
}
|
||||
if !topList.isEmpty {
|
||||
arr.append(XSSearchHotSection(icon: UIImage(named: "hot_icon_04"), title: XSSearchData.hotSectionTitles[1], style: .purple, items: topList))
|
||||
}
|
||||
historyHotView.hotSections = arr
|
||||
|
||||
xs_updateVisibility()
|
||||
}
|
||||
|
||||
private func xs_fetchSuggestions(_ keyword: String) {
|
||||
suggestionTask?.cancel()
|
||||
let expectedKeyword = keyword
|
||||
suggestionTask = Task { [weak self] in
|
||||
// 联想词同样走搜索接口
|
||||
let list = await XSHomeAPI.requestSearch(text: keyword) ?? []
|
||||
guard !Task.isCancelled else { return }
|
||||
await MainActor.run {
|
||||
guard let self = self,
|
||||
self.mode == .suggest,
|
||||
self.currentSuggestKeyword == expectedKeyword else {
|
||||
return
|
||||
}
|
||||
self.suggestionItems = list
|
||||
self.suggestionCollectionView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func xs_fetchResults(_ keyword: String) {
|
||||
resultTask?.cancel()
|
||||
let expectedKeyword = keyword
|
||||
resultTask = Task { [weak self] in
|
||||
// 搜索结果走搜索接口
|
||||
let list = await XSHomeAPI.requestSearch(text: keyword) ?? []
|
||||
guard !Task.isCancelled else { return }
|
||||
await MainActor.run {
|
||||
guard let self = self,
|
||||
self.mode == .result,
|
||||
self.currentResultKeyword == expectedKeyword else {
|
||||
return
|
||||
}
|
||||
self.resultItems = list
|
||||
self.resultCollectionView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
extension XSSearchViewController {
|
||||
|
||||
private func xs_handleSuggest(_ text: String?) {
|
||||
let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if keyword.isEmpty {
|
||||
suggestionTask?.cancel()
|
||||
resultTask?.cancel()
|
||||
currentSuggestKeyword = ""
|
||||
mode = .idle
|
||||
suggestionItems = []
|
||||
suggestionCollectionView.reloadData()
|
||||
return
|
||||
}
|
||||
|
||||
resultTask?.cancel()
|
||||
resultItems = []
|
||||
resultCollectionView.reloadData()
|
||||
resultCollectionView.isHidden = true
|
||||
currentSuggestKeyword = keyword
|
||||
mode = .suggest
|
||||
suggestionCollectionView.isHidden = false
|
||||
xs_fetchSuggestions(keyword)
|
||||
}
|
||||
|
||||
private func xs_submitSearch(_ text: String?) {
|
||||
let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !keyword.isEmpty else {
|
||||
mode = .idle
|
||||
return
|
||||
}
|
||||
|
||||
suggestionTask?.cancel()
|
||||
suggestionItems = []
|
||||
suggestionCollectionView.reloadData()
|
||||
suggestionCollectionView.isHidden = true
|
||||
currentResultKeyword = keyword
|
||||
view.endEditing(true)
|
||||
mode = .result
|
||||
xs_addHistory(keyword)
|
||||
xs_fetchResults(keyword)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - History
|
||||
extension XSSearchViewController {
|
||||
|
||||
private func xs_loadHistory() {
|
||||
history = UserDefaults.standard.stringArray(forKey: XSSearchData.historyKey) ?? []
|
||||
historyHotView.historyTags = history
|
||||
xs_updateVisibility()
|
||||
}
|
||||
|
||||
private func xs_addHistory(_ keyword: String) {
|
||||
// 搜索历史本地缓存,最多保留 10 条
|
||||
history.removeAll { $0.caseInsensitiveCompare(keyword) == .orderedSame }
|
||||
history.insert(keyword, at: 0)
|
||||
if history.count > 10 {
|
||||
history = Array(history.prefix(10))
|
||||
}
|
||||
UserDefaults.standard.set(history, forKey: XSSearchData.historyKey)
|
||||
historyHotView.historyTags = history
|
||||
xs_updateVisibility()
|
||||
}
|
||||
|
||||
private func xs_clearHistory() {
|
||||
history = []
|
||||
UserDefaults.standard.removeObject(forKey: XSSearchData.historyKey)
|
||||
historyHotView.historyTags = []
|
||||
xs_updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
extension XSSearchViewController {
|
||||
|
||||
private func xs_updateVisibility() {
|
||||
switch mode {
|
||||
case .idle:
|
||||
historyHotView.isHidden = !historyHotView.hasContent
|
||||
suggestionCollectionView.isHidden = true
|
||||
resultCollectionView.isHidden = true
|
||||
headerView.style = .home
|
||||
case .suggest:
|
||||
historyHotView.isHidden = true
|
||||
suggestionCollectionView.isHidden = false
|
||||
resultCollectionView.isHidden = true
|
||||
headerView.style = .input
|
||||
case .result:
|
||||
historyHotView.isHidden = true
|
||||
suggestionCollectionView.isHidden = true
|
||||
resultCollectionView.isHidden = false
|
||||
headerView.style = .input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSSearchViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
if collectionView == suggestionCollectionView {
|
||||
return suggestionItems.count
|
||||
}
|
||||
return resultItems.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
if collectionView == suggestionCollectionView {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSSearchSuggestionCell", for: indexPath) as! XSSearchSuggestionCell
|
||||
let item = suggestionItems[indexPath.row]
|
||||
let keyword = headerView.text ?? ""
|
||||
cell.configure(model: item, keyword: keyword)
|
||||
return cell
|
||||
} else {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSSearchResultCell", for: indexPath) as! XSSearchResultCell
|
||||
cell.configure(model: resultItems[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
if collectionView == suggestionCollectionView {
|
||||
let item = suggestionItems[indexPath.row]
|
||||
xs_pushDetail(model: item)
|
||||
} else if collectionView == resultCollectionView {
|
||||
let item = resultItems[indexPath.row]
|
||||
xs_pushDetail(model: item)
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
if collectionView == suggestionCollectionView {
|
||||
let width = XSScreen.width - 32
|
||||
return CGSize(width: floor(width), height: 104)
|
||||
} else {
|
||||
let width = (XSScreen.width - 32 - 16) / 3
|
||||
return CGSize(width: floor(width), height: 186)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Detail
|
||||
extension XSSearchViewController {
|
||||
|
||||
private func xs_pushDetail(model: XSShortModel) {
|
||||
let shortId = model.short_play_id ?? model.short_play_video_id ?? model.id
|
||||
guard let shortId = shortId, !shortId.isEmpty else { return }
|
||||
let controller = XSShortDetailViewController()
|
||||
controller.shortId = shortId
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
}
|
||||
22
XSeri/Class/Home/Model/XSCategoryModel.swift
Normal file
22
XSeri/Class/Home/Model/XSCategoryModel.swift
Normal file
@ -0,0 +1,22 @@
|
||||
//
|
||||
// XSCategoryModel.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SmartCodable
|
||||
|
||||
struct XSCategoryModel: SmartCodable {
|
||||
|
||||
var id: String?
|
||||
var name: String?
|
||||
|
||||
static func mappingForKey() -> [SmartKeyTransformer]? {
|
||||
return [
|
||||
CodingKeys.id <--- ["id", "category_id"],
|
||||
CodingKeys.name <--- ["name", "category_name"]
|
||||
]
|
||||
}
|
||||
}
|
||||
16
XSeri/Class/Home/Model/XSHomeData.swift
Normal file
16
XSeri/Class/Home/Model/XSHomeData.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
struct XSHomeData {
|
||||
|
||||
/// 首页顶部 Tab 分类
|
||||
static let topTabs = [
|
||||
"Popular",
|
||||
"New",
|
||||
"Rankings",
|
||||
"Categories",
|
||||
"aaaaa",
|
||||
"gfgfggg",
|
||||
"ffffff"
|
||||
]
|
||||
|
||||
}
|
||||
66
XSeri/Class/Home/Model/XSHomeModuleItem.swift
Normal file
66
XSeri/Class/Home/Model/XSHomeModuleItem.swift
Normal file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// XSHomeModuleItem.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SmartCodable
|
||||
|
||||
class XSHomeModuleItem: 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"
|
||||
}
|
||||
|
||||
required override init() { }
|
||||
|
||||
var module_key: ModuleKey?
|
||||
var title: String?
|
||||
var list: [XSShortModel]?
|
||||
|
||||
@SmartAny
|
||||
var data: Any?
|
||||
|
||||
@IgnoredKey
|
||||
var iconImage: UIImage?
|
||||
|
||||
@IgnoredKey
|
||||
var br_cellHeight: CGFloat?
|
||||
|
||||
|
||||
|
||||
func didFinishMapping() {
|
||||
if let data = data as? [[String : Any]] {
|
||||
self.list = [XSShortModel].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 = [XSShortModel].deserialize(from: dataList)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
41
XSeri/Class/Home/Model/XSSearchData.swift
Normal file
41
XSeri/Class/Home/Model/XSSearchData.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// XSSearchData.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// 搜索页静态数据,集中管理文字与列表,避免视图中硬编码
|
||||
struct XSSearchData {
|
||||
|
||||
/// 搜索框占位文案
|
||||
static let searchPlaceholder = "Love in Ashes is he".localized
|
||||
/// 最近搜索标题
|
||||
static let recentTitle = "Recent".localized
|
||||
/// 热门搜索标题
|
||||
static let hotSearchesTitle = "Hot Searches".localized
|
||||
/// 热门榜单子标题
|
||||
static let hotSectionTitles = [
|
||||
"Rising Popularity".localized,
|
||||
"Top-Rated Charts".localized
|
||||
]
|
||||
/// 搜索历史存储键
|
||||
static let historyKey = "xs_search_history"
|
||||
}
|
||||
|
||||
/// 榜单板块模型
|
||||
struct XSSearchHotSection {
|
||||
let icon: UIImage?
|
||||
let title: String
|
||||
let style: XSSearchHotSectionStyle
|
||||
let items: [XSShortModel]
|
||||
}
|
||||
|
||||
/// 榜单样式类型
|
||||
enum XSSearchHotSectionStyle {
|
||||
case gold
|
||||
case purple
|
||||
}
|
||||
69
XSeri/Class/Home/View/XSHomeCategoriesCell.swift
Normal file
69
XSeri/Class/Home/View/XSHomeCategoriesCell.swift
Normal file
@ -0,0 +1,69 @@
|
||||
//
|
||||
// XSHomeCategoriesCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeCategoriesCell: UICollectionViewCell {
|
||||
|
||||
var model: XSShortModel? {
|
||||
didSet {
|
||||
coverImageView.xs_setImage(model?.image_url)
|
||||
titleLabel.text = model?.name
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 10
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 10, weight: .semibold)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
contentView.layer.cornerRadius = 10
|
||||
contentView.layer.masksToBounds = true
|
||||
contentView.layer.borderWidth = 1
|
||||
contentView.layer.borderColor = UIColor._6_D_71_E_0.withAlphaComponent(0.4).cgColor
|
||||
|
||||
xs_layoutUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSHomeCategoriesCell {
|
||||
|
||||
private func xs_layoutUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-40)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(5)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-5)
|
||||
make.top.equalTo(coverImageView.snp.bottom).offset(6)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
121
XSeri/Class/Home/View/XSHomeCategoriesHeaderView.swift
Normal file
121
XSeri/Class/Home/View/XSHomeCategoriesHeaderView.swift
Normal file
@ -0,0 +1,121 @@
|
||||
//
|
||||
// XSHomeCategoriesHeaderView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import collection_view_layouts
|
||||
import SnapKit
|
||||
|
||||
class XSHomeCategoriesHeaderView: UICollectionReusableView {
|
||||
|
||||
|
||||
var didChangeHeight: ((_ height: CGFloat) -> Void)?
|
||||
|
||||
var didSelected: ((_ index: Int) -> Void)?
|
||||
|
||||
var dataArr: [XSCategoryModel] = [] {
|
||||
didSet {
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
var currentIndex: Int = 0 {
|
||||
didSet {
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var layout: TagsLayout = {
|
||||
let layout = TagsLayout()
|
||||
layout.delegate = self
|
||||
layout.cellsPadding = .init(horizontal: 12, vertical: 16)
|
||||
layout.contentPadding = .init(horizontal: 16, vertical: 0)
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: XSCollectionView = {
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.isScrollEnabled = false
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
|
||||
collectionView.register(XSHomeCategoriesTagsCell.self, forCellWithReuseIdentifier: "cell")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
self.collectionView.removeObserver(self, forKeyPath: "contentSize")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_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.didChangeHeight?(self.collectionView.contentSize.height + 14)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSHomeCategoriesHeaderView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(collectionView)
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
make.top.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-14)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSHomeCategoriesHeaderView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSHomeCategoriesTagsCell
|
||||
cell.xs_isSelected = currentIndex == indexPath.row
|
||||
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) {
|
||||
guard currentIndex != indexPath.row else { return }
|
||||
self.currentIndex = indexPath.row
|
||||
self.collectionView.reloadData()
|
||||
|
||||
self.didSelected?(indexPath.row)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: LayoutDelegate
|
||||
extension XSHomeCategoriesHeaderView: LayoutDelegate {
|
||||
|
||||
func cellSize(indexPath: IndexPath) -> CGSize {
|
||||
let model = self.dataArr[indexPath.row]
|
||||
let text = model.name ?? ""
|
||||
let width = text.size(XSHomeCategoriesTagsCell.textFont, .init(width: XSScreen.width, height: 27)).width
|
||||
|
||||
return .init(width: width + 24, height: 27)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
81
XSeri/Class/Home/View/XSHomeCategoriesTagsCell.swift
Normal file
81
XSeri/Class/Home/View/XSHomeCategoriesTagsCell.swift
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// XSHomeCategoriesTagsCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeCategoriesTagsCell: UICollectionViewCell {
|
||||
static let textFont = UIFont.font(ofSize: 12, weight: .regular)
|
||||
static let selectedTextFont = UIFont.font(ofSize: 12, weight: .bold)
|
||||
static let textColor = UIColor.white
|
||||
static let selectedTextColor = UIColor._282828
|
||||
|
||||
|
||||
var model: XSCategoryModel? {
|
||||
didSet {
|
||||
titleLabel.text = model?.name
|
||||
}
|
||||
}
|
||||
|
||||
var xs_isSelected: Bool = false {
|
||||
didSet {
|
||||
if xs_isSelected {
|
||||
titleLabel.font = Self.selectedTextFont
|
||||
titleLabel.textColor = Self.selectedTextColor
|
||||
bgView.isHidden = false
|
||||
} else {
|
||||
titleLabel.font = Self.textFont
|
||||
titleLabel.textColor = Self.textColor
|
||||
bgView.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var bgView: UIImageView = {
|
||||
let view = UIImageView(image: UIImage(named: "gradient_color_image_01"))
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.contentView.backgroundColor = .FFEFD_8.withAlphaComponent(0.15)
|
||||
self.contentView.layer.masksToBounds = true
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.contentView.layer.cornerRadius = self.contentView.bounds.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomeCategoriesTagsCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(bgView)
|
||||
contentView.addSubview(titleLabel)
|
||||
|
||||
bgView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
103
XSeri/Class/Home/View/XSHomeNewBigCell.swift
Normal file
103
XSeri/Class/Home/View/XSHomeNewBigCell.swift
Normal file
@ -0,0 +1,103 @@
|
||||
//
|
||||
// XSHomeNewBigCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeNewBigCell: UICollectionViewCell {
|
||||
|
||||
var model: XSShortModel? {
|
||||
didSet {
|
||||
coverBigImageView.xs_setImage(model?.image_url)
|
||||
coverImageView.xs_setImage(model?.image_url)
|
||||
titleLabel.text = model?.name
|
||||
categoryLabel.text = model?.category?.first
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var coverBigImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
imageView.layer.borderWidth = 1
|
||||
imageView.layer.borderColor = UIColor.white.withAlphaComponent(0.25).cgColor
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .medium)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var categoryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .regular)
|
||||
label.textColor = .white.withAlphaComponent(0.5)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.titleLabel.text = "What Goes Around Comes Around"
|
||||
self.categoryLabel.text = "Antiquity"
|
||||
|
||||
contentView.backgroundColor = .white.withAlphaComponent(0.06)
|
||||
contentView.layer.cornerRadius = 8
|
||||
contentView.layer.masksToBounds = true
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSHomeNewBigCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(coverBigImageView)
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(categoryLabel)
|
||||
|
||||
coverBigImageView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
make.height.equalTo(280)
|
||||
}
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(9)
|
||||
make.top.equalTo(coverBigImageView.snp.bottom).offset(9)
|
||||
make.width.equalTo(45)
|
||||
make.height.equalTo(60)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(coverImageView.snp.right).offset(7)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-10)
|
||||
make.top.equalTo(coverBigImageView.snp.bottom).offset(9)
|
||||
}
|
||||
|
||||
categoryLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(titleLabel)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-10)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(3)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
154
XSeri/Class/Home/View/XSHomeNewCell.swift
Normal file
154
XSeri/Class/Home/View/XSHomeNewCell.swift
Normal file
@ -0,0 +1,154 @@
|
||||
//
|
||||
// XSHomeNewCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeNewCell: UICollectionViewCell {
|
||||
|
||||
var model: XSShortModel? {
|
||||
didSet {
|
||||
coverImageView.xs_setImage(model?.image_url)
|
||||
titleLabel.text = model?.name
|
||||
desLabel.text = model?.xs_description
|
||||
epLabel.text = "## Episodes".localizedReplace(text: "\(model?.episode_total ?? 0)")
|
||||
|
||||
if let category = model?.category?.first, !category.isEmpty {
|
||||
categoryView.isHidden = false
|
||||
categoryLabel.text = category
|
||||
} else {
|
||||
categoryView.isHidden = true
|
||||
}
|
||||
|
||||
if model?.tag_type == .hot {
|
||||
tagView.isHidden = false
|
||||
tagView.image = UIImage(named: "hot_icon_02")
|
||||
} else if model?.tag_type == .new {
|
||||
tagView.isHidden = false
|
||||
tagView.image = UIImage(named: "new_icon_01")
|
||||
} else {
|
||||
tagView.isHidden = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var tagView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.xs_setCornerRadius(topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 4)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 16, weight: .medium)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var epLabel: UILabel = {
|
||||
let label = XSLabel()
|
||||
label.font = .font(ofSize: 14, weight: .regular)
|
||||
label.colorImage = UIImage(named: "gradient_color_image_01")
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var desLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .regular)
|
||||
label.textColor = .white.withAlphaComponent(0.8)
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var categoryView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.cornerRadius = 2
|
||||
view.layer.masksToBounds = true
|
||||
view.backgroundColor = .D_9_D_9_D_9.withAlphaComponent(0.16)
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var categoryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 10, weight: .regular)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
titleLabel.text = "The Year You Were Gone fro...fdsafdafdafdsfsafdsafdasfasdfsa"
|
||||
epLabel.text = "55 Episodes"
|
||||
desLabel.text = "Maddie, a maid, seeks justice for her murdered father. She u...fdsafdasfsfdads"
|
||||
categoryLabel.text = "Angst Romance"
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomeNewCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
coverImageView.addSubview(tagView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(epLabel)
|
||||
contentView.addSubview(desLabel)
|
||||
contentView.addSubview(categoryView)
|
||||
categoryView.addSubview(categoryLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.top.bottom.equalToSuperview()
|
||||
make.width.equalTo(105)
|
||||
}
|
||||
|
||||
tagView.snp.makeConstraints { make in
|
||||
make.left.top.equalToSuperview()
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(coverImageView.snp.right).offset(12)
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
make.top.equalToSuperview().offset(5)
|
||||
}
|
||||
|
||||
epLabel.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(epLabel.snp.bottom).offset(6)
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
}
|
||||
|
||||
categoryView.snp.makeConstraints { make in
|
||||
make.left.equalTo(titleLabel)
|
||||
make.top.equalTo(desLabel.snp.bottom).offset(6)
|
||||
make.height.equalTo(18)
|
||||
}
|
||||
|
||||
categoryLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(6)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
34
XSeri/Class/Home/View/XSHomeNewTitleView.swift
Normal file
34
XSeri/Class/Home/View/XSHomeNewTitleView.swift
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// XSHomeNewTitleView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeNewTitleView: UICollectionReusableView {
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .bold)
|
||||
label.textColor = .FFDAA_4
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
addSubview(titleLabel)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-14)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
139
XSeri/Class/Home/View/XSHomePopularBigCell.swift
Normal file
139
XSeri/Class/Home/View/XSHomePopularBigCell.swift
Normal file
@ -0,0 +1,139 @@
|
||||
//
|
||||
// XSHomePopularBigCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomePopularBigCell: UICollectionViewCell {
|
||||
|
||||
var model: XSShortModel? {
|
||||
didSet {
|
||||
coverImageView.xs_setImage(model?.image_url)
|
||||
titleLabel.text = model?.name
|
||||
desLabel.text = model?.xs_description
|
||||
|
||||
if let category = model?.category?.first, !category.isEmpty {
|
||||
categoryLabel.text = category
|
||||
categoryBgView.isHidden = false
|
||||
} else {
|
||||
categoryBgView.isHidden = true
|
||||
}
|
||||
|
||||
if model?.tag_type == .hot {
|
||||
tagView.isHidden = false
|
||||
tagView.image = UIImage(named: "hot_icon_02")
|
||||
} else if model?.tag_type == .new {
|
||||
tagView.isHidden = false
|
||||
tagView.image = UIImage(named: "new_icon_01")
|
||||
} else {
|
||||
tagView.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
imageView.layer.cornerRadius = 4
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var tagView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.xs_setCornerRadius(topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 4)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .bold)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var desLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 10, weight: .regular)
|
||||
label.textColor = .white.withAlphaComponent(0.5)
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var categoryBgView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.cornerRadius = 12
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.borderWidth = 1
|
||||
view.layer.borderColor = UIColor.white.withAlphaComponent(0.25).cgColor
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var categoryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .regular)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSHomePopularBigCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
coverImageView.addSubview(tagView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(desLabel)
|
||||
contentView.addSubview(categoryBgView)
|
||||
categoryBgView.addSubview(categoryLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-92)
|
||||
}
|
||||
|
||||
tagView.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)
|
||||
}
|
||||
|
||||
desLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
}
|
||||
|
||||
categoryBgView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(24)
|
||||
}
|
||||
|
||||
categoryLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(8)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
101
XSeri/Class/Home/View/XSHomePopularCell.swift
Normal file
101
XSeri/Class/Home/View/XSHomePopularCell.swift
Normal file
@ -0,0 +1,101 @@
|
||||
//
|
||||
// XSHomePopularCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomePopularCell: UICollectionViewCell {
|
||||
|
||||
var model: XSShortModel? {
|
||||
didSet {
|
||||
coverImageView.xs_setImage(model?.image_url)
|
||||
titleLabel.text = model?.name
|
||||
categoryLabel.text = model?.category?.first
|
||||
|
||||
if model?.tag_type == .hot {
|
||||
tagView.isHidden = false
|
||||
tagView.image = UIImage(named: "hot_icon_02")
|
||||
} else if model?.tag_type == .new {
|
||||
tagView.isHidden = false
|
||||
tagView.image = UIImage(named: "new_icon_01")
|
||||
} else {
|
||||
tagView.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var tagView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.xs_setCornerRadius(topLeft: 0, topRight: 0, bottomLeft: 4, bottomRight: 0)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .regular)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var categoryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .regular)
|
||||
label.textColor = .white.withAlphaComponent(0.5)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
categoryLabel.text = "Revenge"
|
||||
titleLabel.text = "The Year You Were Gone frfdsafhdaskhfkashfkdsahfdask"
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomePopularCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
coverImageView.addSubview(tagView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(categoryLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-57)
|
||||
}
|
||||
|
||||
tagView.snp.makeConstraints { make in
|
||||
make.right.top.equalToSuperview()
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.top.equalTo(coverImageView.snp.bottom).offset(5)
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
}
|
||||
|
||||
categoryLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
182
XSeri/Class/Home/View/XSHomeRankingsCell.swift
Normal file
182
XSeri/Class/Home/View/XSHomeRankingsCell.swift
Normal file
@ -0,0 +1,182 @@
|
||||
//
|
||||
// XSHomeRankingsCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/31.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeRankingsCell: UICollectionViewCell {
|
||||
|
||||
var model: XSShortModel? {
|
||||
didSet {
|
||||
coverImageView.xs_setImage(model?.image_url)
|
||||
titleLabel.text = model?.name
|
||||
|
||||
if let category = model?.category?.first, !category.isEmpty {
|
||||
categoryLabel.text = category
|
||||
categoryView.isHidden = false
|
||||
} else {
|
||||
categoryView.isHidden = true
|
||||
}
|
||||
|
||||
hotView.setNeedsUpdateConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var num: Int = 0 {
|
||||
didSet {
|
||||
numLabel.text = "\(num)"
|
||||
|
||||
switch num {
|
||||
case 1:
|
||||
numBgView.image = UIImage(named: "rankings_num_bg_01")
|
||||
numLabel.textColor = ._783902
|
||||
|
||||
case 2:
|
||||
numBgView.image = UIImage(named: "rankings_num_bg_02")
|
||||
numLabel.textColor = .white
|
||||
|
||||
default:
|
||||
numBgView.image = UIImage(named: "rankings_num_bg_03")
|
||||
numLabel.textColor = .white
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 3
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var numBgView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var numLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 11, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var hotView: UIButton = {
|
||||
var configuration = UIButton.Configuration.plain()
|
||||
configuration.contentInsets = .zero
|
||||
configuration.image = UIImage(named: "hot_icon_01")
|
||||
configuration.imagePadding = 4
|
||||
configuration.imagePlacement = .leading
|
||||
|
||||
let button = UIButton(configuration: configuration)
|
||||
button.isUserInteractionEnabled = false
|
||||
button.configurationUpdateHandler = { [weak self] button in
|
||||
guard let self = self else { return }
|
||||
|
||||
let string = NSNumber(value: self.model?.watch_total ?? 0).format()
|
||||
|
||||
button.configuration?.attributedTitle = AttributedString(string, attributes: AttributeContainer([
|
||||
.font : UIFont.font(ofSize: 14, weight: .regular),
|
||||
.foregroundColor : UIColor.white
|
||||
]))
|
||||
}
|
||||
button.setContentHuggingPriority(.required, for: .horizontal)
|
||||
button.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .medium)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var categoryView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white.withAlphaComponent(0.08)
|
||||
view.layer.cornerRadius = 2
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var categoryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 10, weight: .regular)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.backgroundColor = .white.withAlphaComponent(0.1)
|
||||
contentView.layer.cornerRadius = 6
|
||||
contentView.layer.masksToBounds = true
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSHomeRankingsCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
|
||||
contentView.addSubview(coverImageView)
|
||||
coverImageView.addSubview(numBgView)
|
||||
numBgView.addSubview(numLabel)
|
||||
contentView.addSubview(hotView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(categoryView)
|
||||
categoryView.addSubview(categoryLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(12)
|
||||
make.width.equalTo(45)
|
||||
make.height.equalTo(60)
|
||||
}
|
||||
|
||||
numBgView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(-3)
|
||||
make.left.equalToSuperview().offset(-3)
|
||||
}
|
||||
|
||||
numLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview().offset(-2)
|
||||
make.centerY.equalToSuperview().offset(-1)
|
||||
}
|
||||
|
||||
hotView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.right.equalToSuperview().offset(-12)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(coverImageView.snp.right).offset(19)
|
||||
make.right.lessThanOrEqualTo(hotView.snp.left).offset(-15)
|
||||
make.top.equalToSuperview().offset(12)
|
||||
}
|
||||
|
||||
categoryView.snp.makeConstraints { make in
|
||||
make.left.equalTo(titleLabel)
|
||||
make.right.lessThanOrEqualTo(hotView.snp.left).offset(-15)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(10)
|
||||
make.height.equalTo(18)
|
||||
}
|
||||
|
||||
categoryLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(6)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
58
XSeri/Class/Home/View/XSHomeSearchButton.swift
Normal file
58
XSeri/Class/Home/View/XSHomeSearchButton.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// XSHomeSearchButton.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2025/12/30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSHomeSearchButton: UIControl {
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return .init(width: XSScreen.width, height: 32)
|
||||
}
|
||||
|
||||
private lazy var iconImageView = UIImageView(image: UIImage(named: "search_icon_01"))
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .medium)
|
||||
label.textColor = .white.withAlphaComponent(0.6)
|
||||
label.text = "Through the Storm".localized
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white.withAlphaComponent(0.1)
|
||||
layer.cornerRadius = 8
|
||||
layer.masksToBounds = true
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSHomeSearchButton {
|
||||
private func xs_setupUI() {
|
||||
addSubview(iconImageView)
|
||||
addSubview(titleLabel)
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(12)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalTo(iconImageView.snp.right).offset(8)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-12)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
XSeri/Class/Home/View/XSSearchGradientButton.swift
Normal file
58
XSeri/Class/Home/View/XSSearchGradientButton.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// XSSearchGradientButton.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
/// 搜索按钮(渐变背景)
|
||||
final class XSSearchGradientButton: UIControl {
|
||||
|
||||
private let gradientLayer = CAGradientLayer()
|
||||
|
||||
private lazy var iconImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "search_icon_02"))
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
return imageView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
gradientLayer.frame = bounds
|
||||
layer.cornerRadius = bounds.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchGradientButton {
|
||||
|
||||
private func xs_setupUI() {
|
||||
// 渐变用于模拟 Figma 中的金色按钮
|
||||
gradientLayer.colors = [
|
||||
UIColor.F_5_BD_7_E.cgColor,
|
||||
UIColor.FFEABC.cgColor,
|
||||
UIColor.FFCF_99.cgColor
|
||||
]
|
||||
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
||||
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
||||
layer.insertSublayer(gradientLayer, at: 0)
|
||||
layer.masksToBounds = true
|
||||
|
||||
addSubview(iconImageView)
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
231
XSeri/Class/Home/View/XSSearchHeaderView.swift
Normal file
231
XSeri/Class/Home/View/XSSearchHeaderView.swift
Normal file
@ -0,0 +1,231 @@
|
||||
//
|
||||
// XSSearchHeaderView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchHeaderView: UIView {
|
||||
|
||||
enum Style {
|
||||
case home
|
||||
case input
|
||||
}
|
||||
|
||||
var text: String? {
|
||||
get { textField.text }
|
||||
set {
|
||||
textField.text = newValue
|
||||
xs_updateSearchButtonVisibility(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回按钮回调
|
||||
var didTapBack: (() -> Void)?
|
||||
/// 搜索按钮回调
|
||||
var didTapSearch: ((_ keyword: String?) -> Void)?
|
||||
/// 输入开始回调
|
||||
var didBeginEditing: ((_ keyword: String?) -> Void)?
|
||||
/// 输入变化回调
|
||||
var didChangeText: ((_ keyword: String?) -> Void)?
|
||||
|
||||
var style: Style = .home {
|
||||
didSet {
|
||||
xs_applyStyle()
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: String? {
|
||||
didSet {
|
||||
// 使用富文本占位,控制颜色与透明度
|
||||
let text = placeholder ?? ""
|
||||
textField.attributedPlaceholder = NSAttributedString(
|
||||
string: text,
|
||||
attributes: [
|
||||
.foregroundColor: UIColor.white.withAlphaComponent(0.6),
|
||||
.font: UIFont.font(ofSize: 14, weight: .medium)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var backButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setImage(UIImage(named: "arrow_left_icon_02"), for: .normal)
|
||||
button.tintColor = .white
|
||||
button.addTarget(self, action: #selector(xs_handleBack), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var searchContainer: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.white.withAlphaComponent(0.18)
|
||||
view.layer.cornerRadius = 19
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var searchIconView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "search_icon_01"))
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var textField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.textColor = .white
|
||||
textField.font = .font(ofSize: 14, weight: .medium)
|
||||
textField.clearButtonMode = .never
|
||||
textField.returnKeyType = .search
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(xs_textChanged), for: .editingChanged)
|
||||
return textField
|
||||
}()
|
||||
|
||||
private lazy var searchButton = XSSearchGradientButton()
|
||||
private var containerHeightConstraint: Constraint?
|
||||
private var textLeadingToSuperview: Constraint?
|
||||
private var textLeadingToIcon: Constraint?
|
||||
private var textTrailingToButton: Constraint?
|
||||
private var textTrailingToSuperview: Constraint?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func becomeFirstResponder() -> Bool {
|
||||
let result = super.becomeFirstResponder()
|
||||
self.textField.becomeFirstResponder()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHeaderView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(backButton)
|
||||
addSubview(searchContainer)
|
||||
searchContainer.addSubview(searchIconView)
|
||||
searchContainer.addSubview(textField)
|
||||
searchContainer.addSubview(searchButton)
|
||||
|
||||
searchButton.addTarget(self, action: #selector(xs_handleSearch), for: .touchUpInside)
|
||||
|
||||
backButton.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(CGSize(width: 24, height: 24))
|
||||
}
|
||||
|
||||
searchContainer.snp.makeConstraints { make in
|
||||
make.left.equalTo(backButton.snp.right).offset(8)
|
||||
make.right.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
containerHeightConstraint = make.height.equalTo(38).constraint
|
||||
}
|
||||
|
||||
searchIconView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(12)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(CGSize(width: 16, height: 16))
|
||||
}
|
||||
|
||||
searchButton.snp.makeConstraints { make in
|
||||
make.right.equalToSuperview().offset(-4)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(CGSize(width: 57, height: 30))
|
||||
}
|
||||
|
||||
textField.snp.makeConstraints { make in
|
||||
textLeadingToSuperview = make.left.equalToSuperview().offset(20).constraint
|
||||
textLeadingToIcon = make.left.equalTo(searchIconView.snp.right).offset(8).constraint
|
||||
make.centerY.equalToSuperview()
|
||||
textTrailingToButton = make.right.equalTo(searchButton.snp.left).offset(-8).constraint
|
||||
textTrailingToSuperview = make.right.equalToSuperview().offset(-12).constraint
|
||||
make.height.equalTo(30)
|
||||
}
|
||||
|
||||
xs_applyStyle()
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHeaderView {
|
||||
|
||||
@objc private func xs_handleBack() {
|
||||
didTapBack?()
|
||||
}
|
||||
|
||||
@objc private func xs_handleSearch() {
|
||||
didTapSearch?(textField.text)
|
||||
}
|
||||
|
||||
@objc private func xs_textChanged() {
|
||||
let value = textField.text
|
||||
xs_updateSearchButtonVisibility(value)
|
||||
didChangeText?(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHeaderView: UITextFieldDelegate {
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
didBeginEditing?(textField.text)
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
didTapSearch?(textField.text)
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHeaderView {
|
||||
|
||||
private func xs_applyStyle() {
|
||||
switch style {
|
||||
case .home:
|
||||
searchIconView.isHidden = true
|
||||
searchContainer.backgroundColor = UIColor.white.withAlphaComponent(0.18)
|
||||
searchContainer.layer.cornerRadius = 19
|
||||
containerHeightConstraint?.update(offset: 38)
|
||||
textLeadingToSuperview?.activate()
|
||||
textLeadingToIcon?.deactivate()
|
||||
case .input:
|
||||
searchIconView.isHidden = false
|
||||
searchContainer.backgroundColor = UIColor.white.withAlphaComponent(0.1)
|
||||
searchContainer.layer.cornerRadius = 18
|
||||
containerHeightConstraint?.update(offset: 36)
|
||||
textLeadingToSuperview?.deactivate()
|
||||
textLeadingToIcon?.activate()
|
||||
}
|
||||
xs_updateSearchButtonVisibility(textField.text)
|
||||
}
|
||||
|
||||
private func xs_updateSearchButtonVisibility(_ text: String?) {
|
||||
let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let shouldShow: Bool
|
||||
switch style {
|
||||
case .home:
|
||||
shouldShow = true
|
||||
case .input:
|
||||
shouldShow = !keyword.isEmpty
|
||||
}
|
||||
searchButton.isHidden = !shouldShow
|
||||
if shouldShow {
|
||||
textTrailingToSuperview?.deactivate()
|
||||
textTrailingToButton?.activate()
|
||||
} else {
|
||||
textTrailingToButton?.deactivate()
|
||||
textTrailingToSuperview?.activate()
|
||||
}
|
||||
layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
110
XSeri/Class/Home/View/XSSearchHistoryHotView.swift
Normal file
110
XSeri/Class/Home/View/XSSearchHistoryHotView.swift
Normal file
@ -0,0 +1,110 @@
|
||||
//
|
||||
// XSSearchHistoryHotView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
/// 搜索历史 + 热门榜单容器视图
|
||||
final class XSSearchHistoryHotView: UIView {
|
||||
|
||||
/// 是否有可展示内容
|
||||
var hasContent: Bool {
|
||||
return !historyTags.isEmpty || !hotSections.isEmpty
|
||||
}
|
||||
|
||||
/// 清空历史回调
|
||||
var didTapClear: (() -> Void)? {
|
||||
didSet {
|
||||
recentView.didTapClear = didTapClear
|
||||
}
|
||||
}
|
||||
/// 点击历史标签回调
|
||||
var didSelectHistory: ((_ text: String) -> Void)? {
|
||||
didSet {
|
||||
recentView.didSelectTag = didSelectHistory
|
||||
}
|
||||
}
|
||||
|
||||
var historyTitle: String? {
|
||||
didSet {
|
||||
recentView.title = historyTitle
|
||||
}
|
||||
}
|
||||
|
||||
var historyTags: [String] = [] {
|
||||
didSet {
|
||||
recentView.tags = historyTags
|
||||
xs_updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
var hotTitle: String? {
|
||||
didSet {
|
||||
hotSectionView.title = hotTitle
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击热门榜单条目回调
|
||||
var didSelectHot: ((_ model: XSShortModel) -> Void)? {
|
||||
didSet {
|
||||
hotSectionView.didSelectItem = didSelectHot
|
||||
}
|
||||
}
|
||||
|
||||
var hotSections: [XSSearchHotSection] = [] {
|
||||
didSet {
|
||||
hotSectionView.sections = hotSections
|
||||
xs_updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private let recentContainer = UIView()
|
||||
private let recentView = XSSearchRecentView()
|
||||
private let hotSectionView = XSSearchHotSectionView()
|
||||
|
||||
private let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.spacing = 24
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHistoryHotView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(stackView)
|
||||
recentContainer.addSubview(recentView)
|
||||
|
||||
stackView.addArrangedSubview(recentContainer)
|
||||
stackView.addArrangedSubview(hotSectionView)
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
recentView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16))
|
||||
}
|
||||
|
||||
xs_updateVisibility()
|
||||
}
|
||||
|
||||
private func xs_updateVisibility() {
|
||||
recentContainer.isHidden = historyTags.isEmpty
|
||||
hotSectionView.isHidden = hotSections.isEmpty
|
||||
}
|
||||
}
|
||||
108
XSeri/Class/Home/View/XSSearchHotListCardView.swift
Normal file
108
XSeri/Class/Home/View/XSSearchHotListCardView.swift
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// XSSearchHotListCardView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchHotListCardView: UIView {
|
||||
|
||||
/// 榜单卡片数据
|
||||
var section: XSSearchHotSection? {
|
||||
didSet {
|
||||
xs_applySection()
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击榜单条目回调
|
||||
var didSelectItem: ((_ model: XSShortModel) -> Void)?
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var iconImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.spacing = 8
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHotListCardView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
layer.cornerRadius = 12
|
||||
layer.masksToBounds = true
|
||||
layer.borderWidth = 1
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(iconImageView)
|
||||
addSubview(stackView)
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.top.equalToSuperview().offset(20)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(iconImageView)
|
||||
make.left.equalTo(iconImageView.snp.right).offset(2)
|
||||
make.right.lessThanOrEqualToSuperview().offset(-10)
|
||||
}
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
make.top.equalToSuperview().offset(53)
|
||||
}
|
||||
}
|
||||
|
||||
private func xs_applySection() {
|
||||
guard let section = section else { return }
|
||||
titleLabel.text = section.title
|
||||
iconImageView.image = section.icon
|
||||
|
||||
switch section.style {
|
||||
case .gold:
|
||||
layer.borderColor = UIColor.FDE_7_B_8.withAlphaComponent(0.4).cgColor
|
||||
titleLabel.textColor = .FDE_7_B_8
|
||||
case .purple:
|
||||
layer.borderColor = UIColor.E_0_C_6_FF.withAlphaComponent(0.2).cgColor
|
||||
titleLabel.textColor = UIColor.E_0_C_6_FF.withAlphaComponent(0.7)
|
||||
}
|
||||
|
||||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
section.items.enumerated().forEach { index, item in
|
||||
let itemView = XSSearchHotListItemView()
|
||||
itemView.configure(model: item, rank: index + 1, style: section.style)
|
||||
itemView.didTap = { [weak self] model in
|
||||
self?.didSelectItem?(model)
|
||||
}
|
||||
itemView.snp.makeConstraints { make in
|
||||
make.height.equalTo(65)
|
||||
}
|
||||
stackView.addArrangedSubview(itemView)
|
||||
}
|
||||
}
|
||||
}
|
||||
147
XSeri/Class/Home/View/XSSearchHotListItemView.swift
Normal file
147
XSeri/Class/Home/View/XSSearchHotListItemView.swift
Normal file
@ -0,0 +1,147 @@
|
||||
//
|
||||
// XSSearchHotListItemView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchHotListItemView: UIView {
|
||||
|
||||
/// 点击条目回调
|
||||
var didTap: ((_ model: XSShortModel) -> Void)?
|
||||
|
||||
private var model: XSShortModel?
|
||||
|
||||
private lazy var rankBadgeView: XSView = {
|
||||
let view = XSView()
|
||||
view.xs_startPoint = .init(x: 0.5, y: 0)
|
||||
view.xs_endPoint = .init(x: 0.5, y: 1)
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var rankLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 10, weight: .bold)
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 10, weight: .semibold)
|
||||
label.textColor = UIColor.white.withAlphaComponent(0.9)
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var categoryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 10, weight: .medium)
|
||||
label.textColor = UIColor.white.withAlphaComponent(0.6)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
rankBadgeView.layer.cornerRadius = rankBadgeView.bounds.height / 2
|
||||
}
|
||||
|
||||
func configure(model: XSShortModel, rank: Int, style: XSSearchHotSectionStyle) {
|
||||
// 根据榜单样式调整数字徽标的颜色
|
||||
self.model = model
|
||||
rankLabel.text = "\(rank)"
|
||||
titleLabel.text = model.name ?? model.xs_description
|
||||
let category = model.category?.first ?? model.categoryList?.first?.name ?? ""
|
||||
categoryLabel.text = category
|
||||
categoryLabel.isHidden = category.isEmpty
|
||||
coverImageView.xs_setImage(model.image_url)
|
||||
|
||||
switch style {
|
||||
case .gold:
|
||||
if rank <= 3 {
|
||||
rankBadgeView.xs_colors = [UIColor.FFE_6_B_3.cgColor, UIColor.F_4_C_783.cgColor]
|
||||
rankLabel.textColor = ._060606
|
||||
} else {
|
||||
rankBadgeView.xs_colors = [UIColor.clear.cgColor, UIColor.clear.cgColor]
|
||||
rankLabel.textColor = .FCD_68_D
|
||||
}
|
||||
case .purple:
|
||||
if rank <= 3 {
|
||||
rankBadgeView.xs_colors = [UIColor.E_7_CCFF.cgColor, UIColor.D_7_BCFF.cgColor, UIColor.A_191_FF.cgColor]
|
||||
rankLabel.textColor = .black
|
||||
} else {
|
||||
rankBadgeView.xs_colors = [UIColor.clear.cgColor, UIColor.clear.cgColor]
|
||||
rankLabel.textColor = UIColor.B_4_A_0_FF
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHotListItemView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(xs_handleTap))
|
||||
addGestureRecognizer(tap)
|
||||
isUserInteractionEnabled = true
|
||||
|
||||
addSubview(rankBadgeView)
|
||||
rankBadgeView.addSubview(rankLabel)
|
||||
addSubview(coverImageView)
|
||||
addSubview(titleLabel)
|
||||
addSubview(categoryLabel)
|
||||
|
||||
rankBadgeView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(CGSize(width: 14, height: 14))
|
||||
}
|
||||
|
||||
rankLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.equalTo(rankBadgeView.snp.right).offset(8)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(CGSize(width: 48, height: 65))
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(coverImageView.snp.right).offset(10)
|
||||
make.top.equalToSuperview()
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
}
|
||||
|
||||
categoryLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(titleLabel)
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(6)
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHotListItemView {
|
||||
|
||||
@objc private func xs_handleTap() {
|
||||
guard let model = model else { return }
|
||||
didTap?(model)
|
||||
}
|
||||
}
|
||||
115
XSeri/Class/Home/View/XSSearchHotSectionView.swift
Normal file
115
XSeri/Class/Home/View/XSSearchHotSectionView.swift
Normal file
@ -0,0 +1,115 @@
|
||||
//
|
||||
// XSSearchHotSectionView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchHotSectionView: UIView {
|
||||
|
||||
var title: String? {
|
||||
didSet {
|
||||
titleLabel.text = title
|
||||
}
|
||||
}
|
||||
|
||||
/// 榜单板块数据
|
||||
var sections: [XSSearchHotSection] = [] {
|
||||
didSet {
|
||||
xs_reloadSections()
|
||||
}
|
||||
}
|
||||
|
||||
/// 点击榜单条目回调
|
||||
var didSelectItem: ((_ model: XSShortModel) -> Void)?
|
||||
|
||||
private var cardViews: [XSSearchHotListCardView] = []
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .bold)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var scrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
view.keyboardDismissMode = .onDrag
|
||||
return view
|
||||
}()
|
||||
|
||||
private let contentView = UIView()
|
||||
|
||||
private lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .horizontal
|
||||
view.spacing = 22
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchHotSectionView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(titleLabel)
|
||||
addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
contentView.addSubview(stackView)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
make.top.equalToSuperview()
|
||||
}
|
||||
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.height.equalTo(435)
|
||||
}
|
||||
|
||||
contentView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
make.height.equalToSuperview()
|
||||
}
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.bottom.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
make.height.equalTo(435)
|
||||
}
|
||||
}
|
||||
|
||||
private func xs_reloadSections() {
|
||||
cardViews.forEach { $0.removeFromSuperview() }
|
||||
cardViews = sections.map { _ in XSSearchHotListCardView() }
|
||||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
for (index, section) in sections.enumerated() {
|
||||
let cardView = cardViews[index]
|
||||
cardView.section = section
|
||||
cardView.didSelectItem = { [weak self] model in
|
||||
self?.didSelectItem?(model)
|
||||
}
|
||||
stackView.addArrangedSubview(cardView)
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.width.equalTo(237)
|
||||
make.height.equalTo(435)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
XSeri/Class/Home/View/XSSearchRecentView.swift
Normal file
101
XSeri/Class/Home/View/XSSearchRecentView.swift
Normal file
@ -0,0 +1,101 @@
|
||||
//
|
||||
// XSSearchRecentView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchRecentView: UIView {
|
||||
|
||||
/// 清空历史回调
|
||||
var didTapClear: (() -> Void)?
|
||||
/// 点击历史标签回调
|
||||
var didSelectTag: ((_ text: String) -> Void)? {
|
||||
didSet {
|
||||
tagsView.didSelectTag = didSelectTag
|
||||
}
|
||||
}
|
||||
|
||||
var title: String? {
|
||||
didSet {
|
||||
titleLabel.text = title
|
||||
}
|
||||
}
|
||||
|
||||
/// 最近搜索标签数据
|
||||
var tags: [String] = [] {
|
||||
didSet {
|
||||
tagsView.tags = tags
|
||||
}
|
||||
}
|
||||
|
||||
private var tagsHeightConstraint: Constraint?
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .bold)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var clearButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setImage(UIImage(named: "delete_icon_01"), for: .normal)
|
||||
button.tintColor = .white.withAlphaComponent(0.6)
|
||||
button.addTarget(self, action: #selector(xs_handleClear), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var tagsView: XSSearchTagsView = {
|
||||
let view = XSSearchTagsView()
|
||||
view.didChangeHeight = { [weak self] height in
|
||||
self?.tagsHeightConstraint?.update(offset: height)
|
||||
}
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchRecentView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(titleLabel)
|
||||
addSubview(clearButton)
|
||||
addSubview(tagsView)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.top.equalToSuperview()
|
||||
}
|
||||
|
||||
clearButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(titleLabel)
|
||||
make.right.equalToSuperview()
|
||||
make.size.equalTo(CGSize(width: 20, height: 20))
|
||||
}
|
||||
|
||||
tagsView.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
tagsHeightConstraint = make.height.equalTo(0).constraint
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchRecentView {
|
||||
|
||||
@objc private func xs_handleClear() {
|
||||
didTapClear?()
|
||||
}
|
||||
}
|
||||
63
XSeri/Class/Home/View/XSSearchResultCell.swift
Normal file
63
XSeri/Class/Home/View/XSSearchResultCell.swift
Normal file
@ -0,0 +1,63 @@
|
||||
//
|
||||
// XSSearchResultCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchResultCell: UICollectionViewCell {
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.placeholderColor = ._1_F_0_B_00
|
||||
imageView.layer.cornerRadius = 8
|
||||
imageView.layer.borderWidth = 1
|
||||
imageView.layer.borderColor = UIColor.white.withAlphaComponent(0.25).cgColor
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .bold)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 2
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(model: XSShortModel) {
|
||||
coverImageView.xs_setImage(model.image_url)
|
||||
titleLabel.text = model.name ?? model.xs_description
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchResultCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.top.right.equalToSuperview()
|
||||
make.height.equalTo(146)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
make.top.equalTo(coverImageView.snp.bottom).offset(8)
|
||||
make.bottom.lessThanOrEqualToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
146
XSeri/Class/Home/View/XSSearchSuggestionCell.swift
Normal file
146
XSeri/Class/Home/View/XSSearchSuggestionCell.swift
Normal file
@ -0,0 +1,146 @@
|
||||
//
|
||||
// XSSearchSuggestionCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchSuggestionCell: UICollectionViewCell {
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.placeholderColor = ._1_F_0_B_00
|
||||
imageView.layer.cornerRadius = 7
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .semibold)
|
||||
label.textColor = UIColor.white.withAlphaComponent(0.4)
|
||||
label.numberOfLines = 1
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var tagContainer: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.white.withAlphaComponent(0.1)
|
||||
view.layer.cornerRadius = 6
|
||||
view.layer.masksToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var tagLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .regular)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var epLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .regular)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var infoStack: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 23
|
||||
stackView.alignment = .leading
|
||||
return stackView
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(model: XSShortModel, keyword: String) {
|
||||
coverImageView.xs_setImage(model.image_url)
|
||||
let title = model.name ?? model.xs_description ?? ""
|
||||
titleLabel.attributedText = xs_highlight(text: title, keyword: keyword)
|
||||
let category = model.category?.first ?? model.categoryList?.first?.name ?? ""
|
||||
tagLabel.text = category
|
||||
tagContainer.isHidden = category.isEmpty
|
||||
if let current = model.current_episode, !current.isEmpty {
|
||||
epLabel.text = "EP.##".localizedReplace(text: current)
|
||||
} else if let total = model.episode_total {
|
||||
epLabel.text = "EP.##".localizedReplace(text: "\(total)")
|
||||
} else {
|
||||
epLabel.text = ""
|
||||
}
|
||||
epLabel.isHidden = (epLabel.text ?? "").isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchSuggestionCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(infoStack)
|
||||
tagContainer.addSubview(tagLabel)
|
||||
|
||||
infoStack.addArrangedSubview(titleLabel)
|
||||
infoStack.addArrangedSubview(tagContainer)
|
||||
infoStack.addArrangedSubview(epLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.top.bottom.equalToSuperview()
|
||||
make.size.equalTo(CGSize(width: 79, height: 104))
|
||||
}
|
||||
|
||||
infoStack.snp.makeConstraints { make in
|
||||
make.left.equalTo(coverImageView.snp.right).offset(12)
|
||||
make.centerY.equalToSuperview()
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
|
||||
tagContainer.snp.makeConstraints { make in
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
|
||||
tagLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(12)
|
||||
make.right.equalToSuperview().offset(-12)
|
||||
}
|
||||
}
|
||||
|
||||
private func xs_highlight(text: String, keyword: String) -> NSAttributedString {
|
||||
let baseColor = UIColor.white.withAlphaComponent(0.4)
|
||||
let highlightColor = UIColor.white
|
||||
let attributed = NSMutableAttributedString(
|
||||
string: text,
|
||||
attributes: [
|
||||
.foregroundColor: baseColor
|
||||
]
|
||||
)
|
||||
|
||||
guard !keyword.isEmpty else { return attributed }
|
||||
let lowerText = text.lowercased()
|
||||
let lowerKeyword = keyword.lowercased()
|
||||
var searchRange = lowerText.startIndex..<lowerText.endIndex
|
||||
|
||||
// 高亮关键词
|
||||
while let range = lowerText.range(of: lowerKeyword, options: [], range: searchRange) {
|
||||
let nsRange = NSRange(range, in: text)
|
||||
attributed.addAttribute(.foregroundColor, value: highlightColor, range: nsRange)
|
||||
searchRange = range.upperBound..<lowerText.endIndex
|
||||
}
|
||||
return attributed
|
||||
}
|
||||
}
|
||||
54
XSeri/Class/Home/View/XSSearchTagCell.swift
Normal file
54
XSeri/Class/Home/View/XSSearchTagCell.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// XSSearchTagCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
final class XSSearchTagCell: UICollectionViewCell {
|
||||
|
||||
static let textFont = UIFont.font(ofSize: 12, weight: .bold)
|
||||
|
||||
var title: String? {
|
||||
didSet {
|
||||
titleLabel.text = title
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = Self.textFont
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.backgroundColor = UIColor.white.withAlphaComponent(0.18)
|
||||
contentView.layer.masksToBounds = true
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
contentView.layer.cornerRadius = contentView.bounds.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchTagCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(titleLabel)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
110
XSeri/Class/Home/View/XSSearchTagsView.swift
Normal file
110
XSeri/Class/Home/View/XSSearchTagsView.swift
Normal file
@ -0,0 +1,110 @@
|
||||
//
|
||||
// XSSearchTagsView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
import collection_view_layouts
|
||||
|
||||
final class XSSearchTagsView: UIView {
|
||||
|
||||
var didChangeHeight: ((_ height: CGFloat) -> Void)?
|
||||
/// 标签点击回调
|
||||
var didSelectTag: ((_ text: String) -> Void)?
|
||||
|
||||
private var lastLayoutWidth: CGFloat = 0
|
||||
|
||||
/// 标签数据列表
|
||||
var tags: [String] = [] {
|
||||
didSet {
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var layout: TagsLayout = {
|
||||
let layout = TagsLayout()
|
||||
layout.delegate = self
|
||||
layout.cellsPadding = .init(horizontal: 12, vertical: 12)
|
||||
layout.contentPadding = .init(horizontal: 0, vertical: 0)
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: XSCollectionView = {
|
||||
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
collectionView.isScrollEnabled = false
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
|
||||
collectionView.register(XSSearchTagCell.self, forCellWithReuseIdentifier: "cell")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
collectionView.removeObserver(self, forKeyPath: "contentSize")
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
xs_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?) {
|
||||
guard keyPath == "contentSize" else { return }
|
||||
didChangeHeight?(collectionView.contentSize.height)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
// 宽度确定后刷新布局,避免标签被纵向堆叠
|
||||
if bounds.width != lastLayoutWidth {
|
||||
lastLayoutWidth = bounds.width
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSSearchTagsView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(collectionView)
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSSearchTagsView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return tags.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSSearchTagCell
|
||||
cell.title = tags[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
didSelectTag?(tags[indexPath.row])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LayoutDelegate
|
||||
extension XSSearchTagsView: LayoutDelegate {
|
||||
|
||||
func cellSize(indexPath: IndexPath) -> CGSize {
|
||||
let text = tags[indexPath.row]
|
||||
let width = text.size(XSSearchTagCell.textFont, .init(width: XSScreen.width, height: 24)).width
|
||||
return .init(width: width + 20, height: 24)
|
||||
}
|
||||
}
|
||||
41
XSeri/Class/Home/ViewModel/XSHomeViewModel.swift
Normal file
41
XSeri/Class/Home/ViewModel/XSHomeViewModel.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// XSHomeViewModel.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/1/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
class XSHomeViewModel: NSObject {
|
||||
|
||||
var dataArr: [XSHomeModuleItem] = []
|
||||
|
||||
|
||||
func requestHomeData() async {
|
||||
guard let list = await XSHomeAPI.requestHomeData() else { return }
|
||||
self.dataArr.removeAll()
|
||||
|
||||
var popularItem: XSHomeModuleItem?
|
||||
var rankingsItem: XSHomeModuleItem?
|
||||
|
||||
list.forEach {
|
||||
if $0.module_key == .week_highest_recommend {
|
||||
popularItem = $0
|
||||
} else if $0.module_key == .week_ranking {
|
||||
rankingsItem = $0
|
||||
}
|
||||
}
|
||||
|
||||
if let item = popularItem {
|
||||
self.dataArr.append(item)
|
||||
}
|
||||
|
||||
if let item = rankingsItem {
|
||||
self.dataArr.append(item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
94
XSeri/Class/Mine/Controller/XSAboutViewController.swift
Normal file
94
XSeri/Class/Mine/Controller/XSAboutViewController.swift
Normal file
@ -0,0 +1,94 @@
|
||||
//
|
||||
// XSAboutViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSAboutViewController: XSCommonViewController {
|
||||
|
||||
private lazy var dataArr: [XSMineItem] = {
|
||||
let arr = [
|
||||
XSMineItem(type: .web, title: "Privacy Policy".localized, url: kXSPrivacyPolicyWebUrl),
|
||||
XSMineItem(type: .web, title: "User Agreement".localized, url: kXSUserAgreementWebUrl),
|
||||
XSMineItem(type: .safari, title: "Visit Website".localized, url: XSWebBaseURL),
|
||||
]
|
||||
return arr
|
||||
}()
|
||||
|
||||
private lazy var tableView: XSTableView = {
|
||||
let tableView = XSTableView(frame: .zero, style: .grouped)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.separatorStyle = .none
|
||||
tableView.register(XSAboutCell.self, forCellReuseIdentifier: "cell")
|
||||
tableView.register(XSAboutHeaderView.self, forHeaderFooterViewReuseIdentifier: "header")
|
||||
return tableView
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.title = "About".localized
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||
self.xs_setNavigationStyle()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSAboutViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(tableView)
|
||||
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalTo(self.view.safeAreaLayoutGuide)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//MARK: UITableViewDelegate UITableViewDataSource
|
||||
extension XSAboutViewController: UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! XSAboutCell
|
||||
cell.item = self.dataArr[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return self.dataArr.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as? XSAboutHeaderView
|
||||
return view
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let item = self.dataArr[indexPath.row]
|
||||
guard let urlStr = item.url else { return }
|
||||
|
||||
if item.type == .web {
|
||||
let vc = XSBaseWebViewController()
|
||||
vc.webUrl = urlStr
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
} else {
|
||||
if let url = URL(string: urlStr) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
97
XSeri/Class/Mine/Controller/XSFeedbackViewController.swift
Normal file
97
XSeri/Class/Mine/Controller/XSFeedbackViewController.swift
Normal file
@ -0,0 +1,97 @@
|
||||
//
|
||||
// XSFeedbackViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSFeedbackViewController: XSAppWebViewController {
|
||||
|
||||
|
||||
private lazy var rightButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(UIImage(named: "feedback_icon_02"), for: .normal)
|
||||
button.addTarget(self, action: #selector(handleRightBarButton), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var redView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .FF_313_B
|
||||
view.layer.cornerRadius = 8
|
||||
view.isHidden = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var redLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .bold)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
self.webUrl = kXSFeedBackHomeWebUrl
|
||||
super.viewDidLoad()
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightButton)
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
Task {
|
||||
await self.requestRedCount()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleRightBarButton() {
|
||||
let vc = XSAppWebViewController()
|
||||
vc.webUrl = kXSFeedBackListWebUrl
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSFeedbackViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
|
||||
rightButton.addSubview(redView)
|
||||
redView.addSubview(redLabel)
|
||||
|
||||
redView.snp.makeConstraints { make in
|
||||
make.height.equalTo(16)
|
||||
make.width.greaterThanOrEqualTo(16)
|
||||
make.top.equalToSuperview().offset(-8)
|
||||
make.right.equalToSuperview().offset(8)
|
||||
}
|
||||
|
||||
redLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.left.greaterThanOrEqualToSuperview().offset(3)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSFeedbackViewController {
|
||||
|
||||
private func requestRedCount() async {
|
||||
guard let model = await XSSettingAPI.requestFeedbackRedCount() else { return }
|
||||
|
||||
if let count = model.feedback_notice_num, count > 0 {
|
||||
self.redView.isHidden = false
|
||||
self.redLabel.text = "\(count)"
|
||||
} else {
|
||||
self.redView.isHidden = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
161
XSeri/Class/Mine/Controller/XSMineViewController.swift
Normal file
161
XSeri/Class/Mine/Controller/XSMineViewController.swift
Normal file
@ -0,0 +1,161 @@
|
||||
//
|
||||
// XSMineViewController.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSMineViewController: XSCommonViewController {
|
||||
|
||||
private lazy var dataArr: [XSMineItem] = []
|
||||
|
||||
private lazy var historyDataArr: [XSShortModel] = []
|
||||
|
||||
private lazy var logoImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "logo_icon_01"))
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var tableView: XSTableView = {
|
||||
let tableView = XSTableView(frame: .zero, style: .grouped)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = 44
|
||||
tableView.xs_addRefreshHeader { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.handleHeaderRefresh(nil)
|
||||
}
|
||||
tableView.register(XSMineCell.self, forCellReuseIdentifier: "cell")
|
||||
tableView.register(XSMineHeaderView.self, forHeaderFooterViewReuseIdentifier: "header")
|
||||
return tableView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: XSLoginManager.userInfoUpdateNotification, object: nil)
|
||||
|
||||
self.createDataArr()
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
self.createDataArr()
|
||||
Task {
|
||||
await XSLoginManager.manager.updateUserInfo()
|
||||
|
||||
await self.requestPlayHistory()
|
||||
}
|
||||
}
|
||||
|
||||
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||
Task {
|
||||
await XSLoginManager.manager.updateUserInfo()
|
||||
await self.requestPlayHistory()
|
||||
self.tableView.xs_endHeaderRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func userInfoUpdateNotification() {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc private func networkStatusDidChangeNotification() {
|
||||
Task {
|
||||
await XSLoginManager.manager.updateUserInfo()
|
||||
await self.requestPlayHistory()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSMineViewController {
|
||||
|
||||
private func xs_setupUI() {
|
||||
view.addSubview(logoImageView)
|
||||
view.addSubview(self.tableView)
|
||||
|
||||
logoImageView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.top.equalTo(self.view.safeAreaLayoutGuide).offset(9)
|
||||
}
|
||||
|
||||
self.tableView.snp.makeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalTo(logoImageView.snp.bottom).offset(16)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: UITableViewDelegate UITableViewDataSource
|
||||
extension XSMineViewController: UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! XSMineCell
|
||||
cell.item = self.dataArr[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return self.dataArr.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as? XSMineHeaderView
|
||||
view?.userInfo = XSLoginManager.manager.userInfo
|
||||
view?.historyDataArr = self.historyDataArr
|
||||
view?.updateLayout()
|
||||
return view
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let item = self.dataArr[indexPath.row]
|
||||
switch item.type {
|
||||
case .about:
|
||||
let vc = XSAboutViewController()
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
case .feedback:
|
||||
let vc = XSFeedbackViewController()
|
||||
self.navigationController?.pushViewController(vc, animated: true)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XSMineViewController {
|
||||
|
||||
private func createDataArr() {
|
||||
let arr = [
|
||||
// XSMineItem(type: .setting, iconImage: UIImage(named: "setting_icon_01"), title: "Setting".localized),
|
||||
XSMineItem(type: .about, iconImage: UIImage(named: "about_icon_01"), title: "About".localized),
|
||||
XSMineItem(type: .feedback, iconImage: UIImage(named: "feedback_icon_01"), title: "Feedback".localized)
|
||||
]
|
||||
self.dataArr = arr
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
private func requestPlayHistory() async {
|
||||
guard let list = await XSVideoAPI.requestPlayHistorys(page: 1) else { return }
|
||||
self.historyDataArr = list
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
}
|
||||
13
XSeri/Class/Mine/Model/XSFeedbackCountModel.swift
Normal file
13
XSeri/Class/Mine/Model/XSFeedbackCountModel.swift
Normal file
@ -0,0 +1,13 @@
|
||||
//
|
||||
// XSFeedbackCountModel.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SmartCodable
|
||||
|
||||
struct XSFeedbackCountModel: SmartCodable {
|
||||
var feedback_notice_num: Int?
|
||||
}
|
||||
27
XSeri/Class/Mine/Model/XSMineItem.swift
Normal file
27
XSeri/Class/Mine/Model/XSMineItem.swift
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// XSMineItem.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct XSMineItem {
|
||||
|
||||
enum ItemType {
|
||||
case setting
|
||||
case about
|
||||
case feedback
|
||||
case web
|
||||
case safari
|
||||
}
|
||||
|
||||
|
||||
var type: ItemType?
|
||||
var iconImage: UIImage?
|
||||
var title: String?
|
||||
var url: String?
|
||||
|
||||
|
||||
}
|
||||
62
XSeri/Class/Mine/View/XSAboutCell.swift
Normal file
62
XSeri/Class/Mine/View/XSAboutCell.swift
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// XSAboutCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSAboutCell: XSTableViewCell {
|
||||
|
||||
var item: XSMineItem? {
|
||||
didSet {
|
||||
titleLabel.text = item?.title
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .semibold)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var lineView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white.withAlphaComponent(0.1)
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
xs_indicatorImageView.image = UIImage(named: "arrow_right_icon_02")
|
||||
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(xs_indicatorImageView)
|
||||
contentView.addSubview(lineView)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(30)
|
||||
}
|
||||
|
||||
xs_indicatorImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.right.equalToSuperview().offset(-30)
|
||||
}
|
||||
|
||||
lineView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(30)
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
make.height.equalTo(0.6)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
64
XSeri/Class/Mine/View/XSAboutHeaderView.swift
Normal file
64
XSeri/Class/Mine/View/XSAboutHeaderView.swift
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// XSAboutHeaderView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSAboutHeaderView: UITableViewHeaderFooterView {
|
||||
|
||||
private lazy var logoImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: UIImage(named: "logo_icon_02"))
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var appNameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 18, weight: .semibold)
|
||||
label.textColor = .white
|
||||
label.text = kXSName
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var versionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .regular)
|
||||
label.textColor = .FDE_7_B_8
|
||||
label.text = "Version " + kXSVersion
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
automaticallyUpdatesBackgroundConfiguration = false
|
||||
|
||||
contentView.addSubview(logoImageView)
|
||||
contentView.addSubview(appNameLabel)
|
||||
contentView.addSubview(versionLabel)
|
||||
|
||||
logoImageView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(30)
|
||||
}
|
||||
|
||||
appNameLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(logoImageView.snp.bottom).offset(7)
|
||||
}
|
||||
|
||||
versionLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(appNameLabel.snp.bottom).offset(7)
|
||||
make.bottom.equalToSuperview().offset(-30)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
66
XSeri/Class/Mine/View/XSMineCell.swift
Normal file
66
XSeri/Class/Mine/View/XSMineCell.swift
Normal file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// XSMineCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSMineCell: XSTableViewCell {
|
||||
|
||||
var item: XSMineItem? {
|
||||
didSet {
|
||||
iconImageView.image = item?.iconImage
|
||||
titleLabel.text = item?.title
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var iconImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .bold)
|
||||
label.textColor = .FFDAA_4
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
@MainActor required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSMineCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(iconImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(xs_indicatorImageView)
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(16)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalToSuperview().offset(44)
|
||||
}
|
||||
|
||||
xs_indicatorImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
78
XSeri/Class/Mine/View/XSMineHeaderView.swift
Normal file
78
XSeri/Class/Mine/View/XSMineHeaderView.swift
Normal file
@ -0,0 +1,78 @@
|
||||
//
|
||||
// XSMineHeaderView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/12.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSMineHeaderView: UITableViewHeaderFooterView {
|
||||
|
||||
var userInfo: XSUserInfo? {
|
||||
didSet {
|
||||
userInfoView.userInfo = userInfo
|
||||
}
|
||||
}
|
||||
|
||||
var historyDataArr: [XSShortModel] = [] {
|
||||
didSet {
|
||||
historyView.dataArr = historyDataArr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private lazy var stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.spacing = 16
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var userInfoView: XSMineUserInfoView = {
|
||||
let view = XSMineUserInfoView()
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var historyView: XSMinePlayHistoryView = {
|
||||
let view = XSMinePlayHistoryView()
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateLayout() {
|
||||
stackView.xs_removeAllArrangedSubview()
|
||||
|
||||
stackView.addArrangedSubview(userInfoView)
|
||||
|
||||
if !historyDataArr.isEmpty {
|
||||
stackView.addArrangedSubview(historyView)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSMineHeaderView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(stackView)
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(6)
|
||||
make.bottom.equalToSuperview().offset(-20)
|
||||
make.left.right.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
86
XSeri/Class/Mine/View/XSMinePlayHistoryCell.swift
Normal file
86
XSeri/Class/Mine/View/XSMinePlayHistoryCell.swift
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// XSMinePlayHistoryCell.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSMinePlayHistoryCell: UICollectionViewCell {
|
||||
|
||||
var model: XSShortModel? {
|
||||
didSet {
|
||||
coverImageView.xs_setImage(model?.image_url)
|
||||
titleLabel.text = model?.name
|
||||
|
||||
let epString = NSMutableAttributedString(string: "EP.\(model?.current_episode ?? "0")")
|
||||
epString.yy_color = .FFDAA_4
|
||||
|
||||
let totalEpString = NSMutableAttributedString(string: "/\(model?.episode_total ?? 0)")
|
||||
totalEpString.yy_color = .white.withAlphaComponent(0.4)
|
||||
|
||||
epString.append(totalEpString)
|
||||
|
||||
epLabel.attributedText = epString
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var coverImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .medium)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 2
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var epLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .medium)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSMinePlayHistoryCell {
|
||||
|
||||
private func xs_setupUI() {
|
||||
contentView.addSubview(coverImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(epLabel)
|
||||
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.left.right.top.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-58)
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.top.equalTo(coverImageView.snp.bottom).offset(12)
|
||||
make.right.lessThanOrEqualToSuperview()
|
||||
}
|
||||
|
||||
epLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview()
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
129
XSeri/Class/Mine/View/XSMinePlayHistoryView.swift
Normal file
129
XSeri/Class/Mine/View/XSMinePlayHistoryView.swift
Normal file
@ -0,0 +1,129 @@
|
||||
//
|
||||
// XSMinePlayHistoryView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSMinePlayHistoryView: UIView {
|
||||
|
||||
|
||||
var dataArr: [XSShortModel] = [] {
|
||||
didSet {
|
||||
self.collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var moreButton: UIControl = {
|
||||
let button = UIButton(primaryAction: UIAction(handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.viewController?.tabBarController?.selectedIndex = 2
|
||||
}))
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var iconImageView = UIImageView(image: UIImage(named: "history_icon_01"))
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 14, weight: .bold)
|
||||
label.textColor = .FFDAA_4
|
||||
label.text = "Browsing History".localized
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var indicatorImageView = UIImageView(image: UIImage(named: "arrow_right_icon_01"))
|
||||
|
||||
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.scrollDirection = .horizontal
|
||||
layout.itemSize = .init(width: 109, height: 203)
|
||||
layout.minimumLineSpacing = 8
|
||||
return layout
|
||||
}()
|
||||
|
||||
private lazy var collectionView: XSCollectionView = {
|
||||
let collectionView = XSCollectionView(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(XSMinePlayHistoryCell.self, forCellWithReuseIdentifier: "cell")
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension XSMinePlayHistoryView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(moreButton)
|
||||
moreButton.addSubview(iconImageView)
|
||||
moreButton.addSubview(titleLabel)
|
||||
moreButton.addSubview(indicatorImageView)
|
||||
addSubview(collectionView)
|
||||
|
||||
moreButton.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
make.top.equalToSuperview()
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
|
||||
iconImageView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.left.equalTo(iconImageView.snp.right).offset(8)
|
||||
}
|
||||
|
||||
indicatorImageView.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
}
|
||||
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
make.top.equalTo(moreButton.snp.bottom).offset(23)
|
||||
make.height.equalTo(self.collectionViewLayout.itemSize.height)
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||
extension XSMinePlayHistoryView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSMinePlayHistoryCell
|
||||
cell.model = self.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 = self.dataArr[indexPath.row]
|
||||
let vc = XSShortDetailViewController()
|
||||
vc.shortId = model.short_play_id
|
||||
self.viewController?.navigationController?.pushViewController(vc, animated: true)
|
||||
}
|
||||
}
|
||||
79
XSeri/Class/Mine/View/XSMineUserInfoView.swift
Normal file
79
XSeri/Class/Mine/View/XSMineUserInfoView.swift
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// XSMineUserInfoView.swift
|
||||
// XSeri
|
||||
//
|
||||
// Created by 长沙鸿瑶 on 2026/2/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SnapKit
|
||||
|
||||
class XSMineUserInfoView: UIView {
|
||||
|
||||
var userInfo: XSUserInfo? {
|
||||
didSet {
|
||||
avatarImageView.xs_setImage(userInfo?.avator, placeholder: UIImage(named: "avatar_placeholder_icon_01"))
|
||||
nickNameLabel.text = userInfo?.getNickName()
|
||||
idLabel.text = "ID: \(userInfo?.customer_id ?? "")"
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var avatarImageView: XSImageView = {
|
||||
let imageView = XSImageView()
|
||||
imageView.layer.cornerRadius = 28
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private lazy var nickNameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 16, weight: .bold)
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var idLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .font(ofSize: 12, weight: .medium)
|
||||
label.textColor = .white.withAlphaComponent(0.6)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
xs_setupUI()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension XSMineUserInfoView {
|
||||
|
||||
private func xs_setupUI() {
|
||||
addSubview(avatarImageView)
|
||||
addSubview(nickNameLabel)
|
||||
addSubview(idLabel)
|
||||
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(16)
|
||||
make.top.equalToSuperview().offset(4)
|
||||
make.height.width.equalTo(56)
|
||||
make.bottom.equalToSuperview().offset(-4)
|
||||
}
|
||||
|
||||
nickNameLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(avatarImageView.snp.right).offset(10)
|
||||
make.top.equalTo(avatarImageView).offset(8)
|
||||
}
|
||||
|
||||
idLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(nickNameLabel)
|
||||
make.bottom.equalTo(avatarImageView).offset(-6)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
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