首次提交
This commit is contained in:
parent
1ff6f4fd4f
commit
d1a1bde3aa
4
.gitignore
vendored
4
.gitignore
vendored
@ -38,10 +38,10 @@ playground.xcworkspace
|
|||||||
# you should judge for yourself, the pros and cons are mentioned at:
|
# you should judge for yourself, the pros and cons are mentioned at:
|
||||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||||
#
|
#
|
||||||
# Pods/
|
Pods/
|
||||||
#
|
#
|
||||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||||
# *.xcworkspace
|
*.xcworkspace
|
||||||
|
|
||||||
# Carthage
|
# Carthage
|
||||||
#
|
#
|
||||||
|
|||||||
1234
Fableon.xcodeproj/project.pbxproj
Normal file
1234
Fableon.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
33
Fableon/App/AppDelegate+FAConfig.swift
Normal file
33
Fableon/App/AppDelegate+FAConfig.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate+FAConfig.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MJRefresh
|
||||||
|
import IQKeyboardManagerSwift
|
||||||
|
import IQKeyboardToolbarManager
|
||||||
|
|
||||||
|
|
||||||
|
extension AppDelegate {
|
||||||
|
|
||||||
|
func fa_config() {
|
||||||
|
UIView.fa_Awake()
|
||||||
|
|
||||||
|
FAToast.config()
|
||||||
|
|
||||||
|
//设置刷新控件的语言
|
||||||
|
MJRefreshConfig.default.languageCode = "en"
|
||||||
|
|
||||||
|
IQKeyboardManager.shared.isEnabled = true
|
||||||
|
IQKeyboardManager.shared.resignOnTouchOutside = true
|
||||||
|
IQKeyboardToolbarManager.shared.isEnabled = false
|
||||||
|
|
||||||
|
let appearance = UINavigationBarAppearance.defaultAppearance()
|
||||||
|
UINavigationBar.appearance().scrollEdgeAppearance = appearance
|
||||||
|
UINavigationBar.appearance().standardAppearance = appearance
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
52
Fableon/App/AppDelegate.swift
Normal file
52
Fableon/App/AppDelegate.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// AppDelegate.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
FANetworkMonitor.manager.startMonitoring()
|
||||||
|
self.fa_config()
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: FANetworkMonitor.networkStatusDidChangeNotification, object: nil)
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 0.2) {
|
||||||
|
if FANetworkMonitor.manager.isReachable == true {
|
||||||
|
FALogin.manager.requestUserInfo(completer: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func networkStatusDidChangeNotification() {
|
||||||
|
|
||||||
|
if FANetworkMonitor.manager.isReachable == true {
|
||||||
|
FALogin.manager.requestUserInfo(completer: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
54
Fableon/App/SceneDelegate.swift
Normal file
54
Fableon/App/SceneDelegate.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// SceneDelegate.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
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 }
|
||||||
|
FATool.windowScene = windowScene
|
||||||
|
|
||||||
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
window?.rootViewController = FATabBarController()
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
43
Fableon/Base/Controller/FANavigationController.swift
Normal file
43
Fableon/Base/Controller/FANavigationController.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// FANavigationController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import FDFullscreenPopGesture
|
||||||
|
|
||||||
|
class FANavigationController: UINavigationController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
fd_fullscreenPopGestureRecognizer.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||||
|
if children.count > 0 {
|
||||||
|
viewController.hidesBottomBarWhenPushed = true
|
||||||
|
}
|
||||||
|
super.pushViewController(viewController, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
|
||||||
|
for (index, value) in viewControllers.enumerated() {
|
||||||
|
if index != 0 {
|
||||||
|
value.hidesBottomBarWhenPushed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.setViewControllers(viewControllers, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return self.topViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
override var childForStatusBarHidden: UIViewController? {
|
||||||
|
return self.topViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
Fableon/Base/Controller/FATabBarController.swift
Normal file
64
Fableon/Base/Controller/FATabBarController.swift
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// FATabBarController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FATabBarController: UITabBarController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
let nav1 = getNavigation(FAHomeViewController(), "Home".localized, UIImage(named: "tabbar_home_icon"), UIImage(named: "tabbar_home_icon_selected"))
|
||||||
|
let nav2 = getNavigation(FARecommendViewController(), "Recommend".localized, UIImage(named: "tabbar_recommend_icon"), UIImage(named: "tabbar_recommend_icon_selected"))
|
||||||
|
let nav3 = getNavigation(FACollectViewController(), "Collect".localized, UIImage(named: "tabbar_collect_icon"), UIImage(named: "tabbar_collect_icon_selected"))
|
||||||
|
let nav4 = getNavigation(FAMeViewController(), "Me".localized, UIImage(named: "tabbar_me_icon"), UIImage(named: "tabbar_me_icon_selected"))
|
||||||
|
|
||||||
|
viewControllers = [nav1, nav2, nav3, nav4]
|
||||||
|
|
||||||
|
|
||||||
|
let appearance = UITabBarAppearance()
|
||||||
|
appearance.backgroundColor = .init(named: .color_0D0D0D)
|
||||||
|
appearance.backgroundImage = UIImage()
|
||||||
|
appearance.shadowColor = .clear
|
||||||
|
appearance.shadowImage = UIImage()
|
||||||
|
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
|
||||||
|
.font : UIFont.font(ofSize: 10, weight: .init(500)),
|
||||||
|
.foregroundColor : UIColor(named: .color_777777)!
|
||||||
|
]
|
||||||
|
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
|
||||||
|
.font : UIFont.font(ofSize: 10, weight: .init(500)),
|
||||||
|
.foregroundColor : UIColor(named: .color_3769FC)!
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
self.tabBar.scrollEdgeAppearance = appearance
|
||||||
|
self.tabBar.standardAppearance = appearance
|
||||||
|
self.tabBar.isTranslucent = false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override var childForStatusBarStyle: UIViewController? {
|
||||||
|
return self.selectedViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
override var childForStatusBarHidden: UIViewController? {
|
||||||
|
return self.selectedViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension FATabBarController {
|
||||||
|
|
||||||
|
private func getNavigation(_ viewController: UIViewController, _ title: String, _ image: UIImage?, _ selectedImage: UIImage?) -> UINavigationController {
|
||||||
|
let nav = FANavigationController(rootViewController: viewController)
|
||||||
|
nav.tabBarItem.title = title
|
||||||
|
nav.tabBarItem.image = image
|
||||||
|
nav.tabBarItem.selectedImage = selectedImage
|
||||||
|
return nav
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
93
Fableon/Base/Controller/FAViewController.swift
Normal file
93
Fableon/Base/Controller/FAViewController.swift
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// FAViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAViewController: UIViewController {
|
||||||
|
|
||||||
|
lazy var bgView: UIView = {
|
||||||
|
let view = UIImageView(image: UIImage(named: "背景"))
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.edgesForExtendedLayout = [.top]
|
||||||
|
|
||||||
|
if let navi = navigationController {
|
||||||
|
if navi.visibleViewController == self {
|
||||||
|
if navi.viewControllers.count > 1 {
|
||||||
|
configNavigationBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.addSubview(bgView)
|
||||||
|
bgView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||||
|
completer?()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFooterRefresh(_ completer: (() -> Void)?) {
|
||||||
|
completer?()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
|
return .lightContent
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
func configNavigationBack(_ imageName: String = "Frame 3011") {
|
||||||
|
let image = UIImage(named: imageName)
|
||||||
|
|
||||||
|
let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleNavigationBack))
|
||||||
|
navigationItem.leftBarButtonItem = leftBarButtonItem
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleNavigationBack() {
|
||||||
|
self.fa_toLastViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_toLastViewController(animated: Bool) {
|
||||||
|
if self.navigationController != nil
|
||||||
|
{
|
||||||
|
if self.navigationController?.viewControllers.count == 1
|
||||||
|
{
|
||||||
|
self.dismiss(animated: animated, completion: nil)
|
||||||
|
} else {
|
||||||
|
self.navigationController?.popViewController(animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if self.presentingViewController != nil {
|
||||||
|
self.dismiss(animated: animated, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIViewController {
|
||||||
|
|
||||||
|
func fa_setNavigationStyle(backgroundColor: UIColor = .clear,
|
||||||
|
titleFont: UIFont = .font(ofSize: 18, weight: .bold),
|
||||||
|
titleColor: UIColor = .FFFFFF,
|
||||||
|
isTranslucent: Bool = true
|
||||||
|
) {
|
||||||
|
self.navigationController?.navigationBar.fa_setTranslucent(isTranslucent: isTranslucent)
|
||||||
|
self.navigationController?.navigationBar.fa_setBackgroundColor(backgroundColor: backgroundColor)
|
||||||
|
self.navigationController?.navigationBar.fa_setTitleTextAttributes(titleTextAttributes: [
|
||||||
|
NSAttributedString.Key.font : titleFont,
|
||||||
|
NSAttributedString.Key.foregroundColor : titleColor
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
Fableon/Base/Define/FADefine.swift
Normal file
39
Fableon/Base/Define/FADefine.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// FADefine.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
///当前系统版本号
|
||||||
|
let kFAOsVersion: String = UIDevice.current.systemVersion
|
||||||
|
let kBRAPPBundleIdentifier: String = (Bundle.main.infoDictionary!["CFBundleIdentifier"] as? String) ?? "0"
|
||||||
|
|
||||||
|
///app版本号
|
||||||
|
let kFAAPPVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0"
|
||||||
|
let kFAAPPBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0"
|
||||||
|
|
||||||
|
let kFAAPPBundleName: String = (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? ""
|
||||||
|
let kFAAPPName: String = (Bundle.main.infoDictionary!["CFBundleDisplayName"] as? String) ?? ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public func fa_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!)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
Fableon/Base/Extension/CGMutablePath+FARoundedCorner.swift
Normal file
65
Fableon/Base/Extension/CGMutablePath+FARoundedCorner.swift
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
//
|
||||||
|
// CGMutablePath+FARoundedCorner.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct FARoundedCorner {
|
||||||
|
var topLeft:CGFloat = 0
|
||||||
|
var topRight:CGFloat = 0
|
||||||
|
var bottomLeft:CGFloat = 0
|
||||||
|
var bottomRight:CGFloat = 0
|
||||||
|
|
||||||
|
public static let zero = FARoundedCorner(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:FARoundedCorner, v2:FARoundedCorner) -> Bool {
|
||||||
|
return v1.bottomLeft == v2.bottomLeft
|
||||||
|
&& v1.bottomRight == v2.bottomRight
|
||||||
|
&& v1.topLeft == v2.topLeft
|
||||||
|
&& v1.topRight == v2.topRight
|
||||||
|
}
|
||||||
|
static func !=(v1:FARoundedCorner, v2:FARoundedCorner) -> Bool {
|
||||||
|
return !(v1 == v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGMutablePath {
|
||||||
|
func addRadiusRectangle(_ circulars: FARoundedCorner, 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
Fableon/Base/Extension/Dictionary+FAAdd.swift
Normal file
23
Fableon/Base/Extension/Dictionary+FAAdd.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Dictionary+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
Fableon/Base/Extension/Font+FAAdd.swift
Normal file
24
Fableon/Base/Extension/Font+FAAdd.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// Font+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUICore
|
||||||
|
|
||||||
|
extension Font {
|
||||||
|
|
||||||
|
static func font(size: CGFloat, weight: Font.Weight) -> Font {
|
||||||
|
return Font.system(size: size, weight: weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIFont {
|
||||||
|
|
||||||
|
static func font(ofSize: CGFloat, weight: UIFont.Weight) -> UIFont {
|
||||||
|
return UIFont.systemFont(ofSize: ofSize, weight: weight)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
Fableon/Base/Extension/String+FAAdd.swift
Normal file
41
Fableon/Base/Extension/String+FAAdd.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// String+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SmartCodable
|
||||||
|
import YYCategories
|
||||||
|
|
||||||
|
extension String: SmartCodable {
|
||||||
|
|
||||||
|
static func timeZone() -> String {
|
||||||
|
let timeZone = NSTimeZone.local as NSTimeZone
|
||||||
|
let timeZoneSecondsFromGMT = timeZone.secondsFromGMT / 3600
|
||||||
|
return String(format: "GMT+0%d:00", timeZoneSecondsFromGMT)
|
||||||
|
}
|
||||||
|
|
||||||
|
func size(_ font: UIFont, _ size: CGSize) -> CGSize {
|
||||||
|
return (self as NSString).size(for: font, size: size, mode: .byWordWrapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
static let color_FFFFFF = "#FFFFFF"
|
||||||
|
static let color_000000 = "#000000"
|
||||||
|
static let color_3769FC = "#3769FC"
|
||||||
|
static let color_777777 = "#777777"
|
||||||
|
static let color_0D0D0D = "#0D0D0D"
|
||||||
|
static let color_81CAFF = "#81CAFF"
|
||||||
|
static let color_20A2FF = "#20A2FF"
|
||||||
|
static let color_DDEDFD = "#DDEDFD"
|
||||||
|
static let color_A8DBFF = "#A8DBFF"
|
||||||
|
static let color_BEDFFF = "#BEDFFF"
|
||||||
|
static let color_52A2F1 = "#52A2F1"
|
||||||
|
static let color_C7DEF5 = "#C7DEF5"
|
||||||
|
static let color_333333 = "#333333"
|
||||||
|
static let color_D9D9D9 = "#D9D9D9"
|
||||||
|
}
|
||||||
80
Fableon/Base/Extension/UI/SwiftUIExtension.swift
Normal file
80
Fableon/Base/Extension/UI/SwiftUIExtension.swift
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// SwiftUIExtension.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FAOnFirstAppear: ViewModifier {
|
||||||
|
@State private var hasAppeared = false
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content.onAppear {
|
||||||
|
if !hasAppeared {
|
||||||
|
hasAppeared = true
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FAGradientBorder: ViewModifier {
|
||||||
|
var colors: [Color] = [.red, .blue]
|
||||||
|
var lineWidth: CGFloat = 4
|
||||||
|
var cornerRadius: CGFloat = 16
|
||||||
|
var startPoint: UnitPoint = .topLeading
|
||||||
|
var endPoint: UnitPoint = .bottomTrailing
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.stroke(
|
||||||
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: colors),
|
||||||
|
startPoint: startPoint,
|
||||||
|
endPoint: endPoint
|
||||||
|
),
|
||||||
|
lineWidth: lineWidth
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func onFirstAppear(perform action: @escaping () -> Void) -> some View {
|
||||||
|
self.modifier(FAOnFirstAppear(action: action))
|
||||||
|
}
|
||||||
|
|
||||||
|
func gradientBorder(
|
||||||
|
colors: [Color] = [.red, .blue],
|
||||||
|
lineWidth: CGFloat = 4,
|
||||||
|
cornerRadius: CGFloat = 16,
|
||||||
|
startPoint: UnitPoint = .topLeading,
|
||||||
|
endPoint: UnitPoint = .bottomTrailing
|
||||||
|
) -> some View {
|
||||||
|
self.modifier(FAGradientBorder(colors: colors,
|
||||||
|
lineWidth: lineWidth,
|
||||||
|
cornerRadius: cornerRadius,
|
||||||
|
startPoint: startPoint,
|
||||||
|
endPoint: endPoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBackground() -> some View {
|
||||||
|
self.background(
|
||||||
|
Image("背景")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.clipped()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
49
Fableon/Base/Extension/UINavigationBar+FAAdd.swift
Normal file
49
Fableon/Base/Extension/UINavigationBar+FAAdd.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// UINavigationBar+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
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.FFFFFF
|
||||||
|
]
|
||||||
|
return navBarAppearance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UINavigationBar {
|
||||||
|
|
||||||
|
|
||||||
|
func fa_setTranslucent(isTranslucent: Bool) {
|
||||||
|
self.isTranslucent = isTranslucent
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_setBackgroundColor(backgroundColor: UIColor?) {
|
||||||
|
let appearance = self.standardAppearance
|
||||||
|
appearance.backgroundColor = backgroundColor
|
||||||
|
self.standardAppearance = appearance
|
||||||
|
self.scrollEdgeAppearance = appearance
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_setTitleTextAttributes(titleTextAttributes: [NSAttributedString.Key : Any]?) {
|
||||||
|
let appearance = self.standardAppearance
|
||||||
|
|
||||||
|
if let titleTextAttributes = titleTextAttributes {
|
||||||
|
appearance.titleTextAttributes = titleTextAttributes
|
||||||
|
}
|
||||||
|
self.scrollEdgeAppearance = appearance
|
||||||
|
self.standardAppearance = appearance
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Fableon/Base/Extension/UIScreen+FAAdd.swift
Normal file
40
Fableon/Base/Extension/UIScreen+FAAdd.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// Screen+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIScreen {
|
||||||
|
|
||||||
|
static var screen: UIScreen {
|
||||||
|
return UIScreen.main
|
||||||
|
}
|
||||||
|
|
||||||
|
static var width: CGFloat {
|
||||||
|
return UIScreen.main.bounds.width
|
||||||
|
}
|
||||||
|
|
||||||
|
static var height: CGFloat {
|
||||||
|
return UIScreen.main.bounds.height
|
||||||
|
}
|
||||||
|
|
||||||
|
static var safeTop: CGFloat {
|
||||||
|
return FATool.keyWindow?.safeAreaInsets.top ?? 20
|
||||||
|
}
|
||||||
|
|
||||||
|
static var safeBottom: CGFloat {
|
||||||
|
return FATool.keyWindow?.safeAreaInsets.bottom ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
static var navBarHeight: CGFloat {
|
||||||
|
return safeTop + 44
|
||||||
|
}
|
||||||
|
|
||||||
|
static var tabBarHeight: CGFloat {
|
||||||
|
return safeBottom + 49
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Fableon/Base/Extension/UIScrollView+FARefresh.swift
Normal file
70
Fableon/Base/Extension/UIScrollView+FARefresh.swift
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// UIScrollView+FARefresh.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MJRefresh
|
||||||
|
|
||||||
|
extension UIScrollView {
|
||||||
|
|
||||||
|
func fa_addRefreshHeader(insetTop: CGFloat = 0, block: (() -> Void)?) {
|
||||||
|
|
||||||
|
self.mj_header = MJRefreshNormalHeader(refreshingBlock: {
|
||||||
|
block?()
|
||||||
|
})
|
||||||
|
self.mj_header?.ignoredScrollViewContentInsetTop = insetTop
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_addRefreshFooter(insetBottom: CGFloat = 0, block: (() -> Void)?) {
|
||||||
|
let footer = MJRefreshAutoNormalFooter(refreshingBlock: {
|
||||||
|
block?()
|
||||||
|
})
|
||||||
|
footer.ignoredScrollViewContentInsetBottom = insetBottom
|
||||||
|
|
||||||
|
self.mj_footer = footer
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func fa_addRefreshBackFooter(insetBottom: CGFloat = 0, block: (() -> Void)?) {
|
||||||
|
self.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: {
|
||||||
|
block?()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.mj_footer?.ignoredScrollViewContentInsetBottom = insetBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_endHeaderRefreshing() {
|
||||||
|
self.mj_header?.endRefreshing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_endFooterRefreshing() {
|
||||||
|
if self.mj_footer?.state == .noMoreData { return }
|
||||||
|
self.mj_footer?.endRefreshing()
|
||||||
|
}
|
||||||
|
|
||||||
|
///重置没有更多
|
||||||
|
func fa_resetNoMoreData() {
|
||||||
|
self.mj_footer?.resetNoMoreData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_endRefreshingWithNoMoreData() {
|
||||||
|
self.mj_footer?.endRefreshingWithNoMoreData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_updateNoMoreDataState(_ hasNextPage: Bool?) {
|
||||||
|
if hasNextPage == false {
|
||||||
|
self.fa_endRefreshingWithNoMoreData()
|
||||||
|
} else {
|
||||||
|
self.fa_resetNoMoreData()
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.mj_totalDataCount() == 0 {
|
||||||
|
self.mj_footer?.isHidden = true
|
||||||
|
} else {
|
||||||
|
self.mj_footer?.isHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Fableon/Base/Extension/UIStackView+FAAdd.swift
Normal file
21
Fableon/Base/Extension/UIStackView+FAAdd.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// UIStackView+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
extension UIStackView {
|
||||||
|
|
||||||
|
func fa_removeAllArrangedSubview() {
|
||||||
|
let arrangedSubviews = self.arrangedSubviews
|
||||||
|
|
||||||
|
arrangedSubviews.forEach {
|
||||||
|
self.removeArrangedSubview($0)
|
||||||
|
$0.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Fableon/Base/Extension/UIView+FAAdd.swift
Normal file
95
Fableon/Base/Extension/UIView+FAAdd.swift
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// UIView+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIView {
|
||||||
|
fileprivate struct AssociatedKeys {
|
||||||
|
static var fa_roundedCorner: Int?
|
||||||
|
static var fa_effect: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public static func fa_Awake() {
|
||||||
|
fa_swizzled_instanceMethod("fa", oldClass: self, oldSelector: "layoutSubviews", newClass: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func fa_layoutSubviews() {
|
||||||
|
fa_layoutSubviews()
|
||||||
|
|
||||||
|
_updateRoundedCorner()
|
||||||
|
|
||||||
|
if let effectView = effectView, effectView.frame != self.bounds {
|
||||||
|
effectView.frame = self.bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: -------------- 圆角 --------------
|
||||||
|
extension UIView {
|
||||||
|
|
||||||
|
|
||||||
|
private var roundedCorner: FARoundedCorner? {
|
||||||
|
get {
|
||||||
|
return objc_getAssociatedObject(self, &AssociatedKeys.fa_roundedCorner) as? FARoundedCorner
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.fa_roundedCorner, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///设置圆角
|
||||||
|
func fa_setRoundedCorner(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) {
|
||||||
|
//清空其它设置方法
|
||||||
|
self.roundedCorner = FARoundedCorner(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
|
||||||
|
_updateRoundedCorner()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _updateRoundedCorner() {
|
||||||
|
guard let roundedCorner = self.roundedCorner else { return }
|
||||||
|
let rect = self.bounds
|
||||||
|
|
||||||
|
let path = CGMutablePath()
|
||||||
|
path.addRadiusRectangle(roundedCorner, rect: rect)
|
||||||
|
|
||||||
|
let maskLayer = CAShapeLayer()
|
||||||
|
maskLayer.frame = self.bounds
|
||||||
|
maskLayer.path = path
|
||||||
|
self.layer.mask = maskLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//MARK: -------------- 模糊效果 --------------
|
||||||
|
extension UIView {
|
||||||
|
private var effectView: UIVisualEffectView? {
|
||||||
|
get {
|
||||||
|
return objc_getAssociatedObject(self, &AssociatedKeys.fa_effect) as? UIVisualEffectView
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.fa_effect, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///添加模糊效果
|
||||||
|
func fa_addEffectView(style: UIBlurEffect.Style = .light) {
|
||||||
|
if self.effectView == nil {
|
||||||
|
let blur = UIBlurEffect(style: style)
|
||||||
|
let effectView = UIVisualEffectView(effect: blur)
|
||||||
|
effectView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(effectView)
|
||||||
|
self.sendSubviewToBack(effectView)
|
||||||
|
|
||||||
|
self.effectView = effectView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///删除模糊效果
|
||||||
|
func fa_removeEffectView() {
|
||||||
|
self.effectView?.removeFromSuperview()
|
||||||
|
self.effectView = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Fableon/Base/Extension/UserDefaults+FAAdd.swift
Normal file
39
Fableon/Base/Extension/UserDefaults+FAAdd.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// UserDefaults+FAAdd.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension UserDefaults {
|
||||||
|
|
||||||
|
static func fa_setObject(_ obj: NSSecureCoding?, forKey key: String) {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
guard let obj = obj else {
|
||||||
|
defaults.removeObject(forKey: key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let data = try NSKeyedArchiver.archivedData(withRootObject: obj, requiringSecureCoding: true)
|
||||||
|
defaults.set(data, forKey: key)
|
||||||
|
} catch {
|
||||||
|
print("Error archiving object: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fa_object<T: NSObject & NSSecureCoding>(forKey key: String, as type: T.Type) -> T? {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
guard let data = defaults.data(forKey: key) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let object = try NSKeyedUnarchiver.unarchivedObject(ofClass: type, from: data)
|
||||||
|
return object
|
||||||
|
} catch {
|
||||||
|
print("Error unarchiving object: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
161
Fableon/Base/Request/FAAPI/FAAPI.swift
Normal file
161
Fableon/Base/Request/FAAPI/FAAPI.swift
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
//
|
||||||
|
// FAAPI.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension FAAPI {
|
||||||
|
///更新短剧关注状态 [ "state" : isCollect, "id" : shortPlayId,]
|
||||||
|
static let updateShortCollectStateNotification = NSNotification.Name(rawValue: "FAAPI.updateShortCollectStateNotification")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FAAPI {
|
||||||
|
|
||||||
|
static func requestHomeModulesData(completer: ((_ list: [FAHomeModuleItem]?) -> Void)?) {
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/home/all-modules", method: .get) { (response: FANetworkManager.Response<FANetworkManager.List<FAHomeModuleItem>>) in
|
||||||
|
completer?(response.data?.list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func requestShortDetailData(shortPlayId: String, activityId: String? = nil, completer: ((FAShortDetailModel?, Int?, String?) -> Void)?) {
|
||||||
|
|
||||||
|
var parameters: [String : Any] = [
|
||||||
|
"short_play_id" : shortPlayId,
|
||||||
|
"video_id" : "0"
|
||||||
|
]
|
||||||
|
|
||||||
|
if let activityId = activityId {
|
||||||
|
parameters["activity_id"] = activityId
|
||||||
|
}
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/getVideoDetails", method: .get, parameters: parameters) { (response: FANetworkManager.Response<FAShortDetailModel>) in
|
||||||
|
if response.isSuccess {
|
||||||
|
completer?(response.data, response.code, response.msg)
|
||||||
|
} else {
|
||||||
|
completer?(nil, response.code, response.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static func requestCreatePlayHistory(videoId: String?, shortPlayId: String?) {
|
||||||
|
guard let shortPlayId = shortPlayId else { return }
|
||||||
|
|
||||||
|
let parameters = [
|
||||||
|
"video_id" : videoId ?? "0",
|
||||||
|
"short_play_id" : shortPlayId
|
||||||
|
]
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/createHistory", method: .post, parameters: parameters, isToast: false) { (response: FANetworkManager.Response<String>) in
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///历史记录列表
|
||||||
|
static func requestPlayHistorys(page: Int, pageSize: Int = 20, completer: ((_ listModel: FANetworkManager.List<FAShortPlayModel>?) -> Void)?) {
|
||||||
|
|
||||||
|
let parameters = [
|
||||||
|
"current_page" : page,
|
||||||
|
"page_size" : pageSize
|
||||||
|
]
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/myHistorys", method: .get, parameters: parameters, isToast: false) { (response: FANetworkManager.Response<FANetworkManager.List<FAShortPlayModel>>) in
|
||||||
|
completer?(response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///收藏
|
||||||
|
static func requestShortCollect(isCollect: Bool, shortPlayId: String, videoId: String?, isLoding: Bool = true, success: (() -> Void)?, failure: (() -> Void)? = nil) {
|
||||||
|
let path: String
|
||||||
|
if isCollect {
|
||||||
|
path = "/collect"
|
||||||
|
} else {
|
||||||
|
path = "/cancelCollect"
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters: [String : Any] = [
|
||||||
|
"short_play_id" : shortPlayId,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let videoId = videoId {
|
||||||
|
parameters["video_id"] = videoId
|
||||||
|
}
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + path,
|
||||||
|
parameters: parameters,
|
||||||
|
isLoding: true) { (response: FANetworkManager.Response<FAShortDetailModel>) in
|
||||||
|
if response.isSuccess {
|
||||||
|
success?()
|
||||||
|
NotificationCenter.default.post(name: FAAPI.updateShortCollectStateNotification, object: nil, userInfo: [
|
||||||
|
"state" : isCollect,
|
||||||
|
"id" : shortPlayId,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
failure?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///推荐短剧
|
||||||
|
static func requestRecommendVideo(page: Int, completer: ((_ listModel: FANetworkManager.List<FAShortPlayModel>?) -> Void)?) {
|
||||||
|
|
||||||
|
let parameters: [String : Any] = [
|
||||||
|
"page_size" : 20,
|
||||||
|
"current_page" : page
|
||||||
|
]
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/getRecommands",
|
||||||
|
method: .get,
|
||||||
|
parameters: parameters,
|
||||||
|
isLoding: false) { (response: FANetworkManager.Response<FANetworkManager.List<FAShortPlayModel>>) in
|
||||||
|
completer?(response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///收藏列表
|
||||||
|
static func requestCollectList(page: Int, completer: ((_ listModel: FANetworkManager.List<FAShortPlayModel>?) -> Void)?) {
|
||||||
|
let parameters: [String : Any] = [
|
||||||
|
"page_size" : 20,
|
||||||
|
"current_page" : page
|
||||||
|
]
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/myCollections",
|
||||||
|
method: .get,
|
||||||
|
parameters: parameters,
|
||||||
|
isLoding: false) { (response: FANetworkManager.Response<FANetworkManager.List<FAShortPlayModel>>) in
|
||||||
|
completer?(response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///热门搜索
|
||||||
|
static func requestHotSearchData(completer: ((_ list: [FAShortPlayModel]?) -> Void)?) {
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/search/hots",
|
||||||
|
method: .get,
|
||||||
|
parameters: nil,
|
||||||
|
isToast: false) { (response: FANetworkManager.Response<FANetworkManager.List<FAShortPlayModel>>) in
|
||||||
|
completer?(response.data?.list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///搜索
|
||||||
|
static func requestSearch(text: String, completer: ((_ list: [FAShortPlayModel]?) -> Void)?) {
|
||||||
|
let parameters = [
|
||||||
|
"search" : text
|
||||||
|
]
|
||||||
|
|
||||||
|
FANetworkManager.manager.request(FABaseURL + "/search",
|
||||||
|
method: .get,
|
||||||
|
parameters: parameters,
|
||||||
|
isToast: true) { (response: FANetworkManager.Response<FANetworkManager.List<FAShortPlayModel>>) in
|
||||||
|
completer?(response.data?.list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
20
Fableon/Base/Request/FAAPIPath.swift
Normal file
20
Fableon/Base/Request/FAAPIPath.swift
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// FAAPIPath.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
let FABaseURL = "https://api-breeltv.breeltv.com/reel"
|
||||||
|
let FAWebBaseURL = "https://www.breeltv.com"
|
||||||
|
let FACampaignWebURL = "https://campaign.breeltv.com"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///反馈首页
|
||||||
|
let kFAFeedBackHomeWebUrl = FACampaignWebURL + "/pages/leave/index"
|
||||||
|
///反馈列表
|
||||||
|
let kFAFeedBackListWebUrl = FACampaignWebURL + "/pages/leave/list"
|
||||||
|
///反馈详情
|
||||||
|
let kFAFeedBackDetailWebUrl = FACampaignWebURL + "/pages/leave/detail"
|
||||||
95
Fableon/Base/Request/FACryptorService.swift
Normal file
95
Fableon/Base/Request/FACryptorService.swift
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// FACryptorService.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FACryptorService {
|
||||||
|
|
||||||
|
static func decrypt(data: String) -> String {
|
||||||
|
guard data.hasPrefix("$") else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
let decryptedData = deStrBytes(data: data)
|
||||||
|
return String(data: decryptedData, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
static func deStrBytes(data: String) -> Data {
|
||||||
|
let hexData = String(data.dropFirst())
|
||||||
|
var bytes = Data()
|
||||||
|
|
||||||
|
var index = hexData.startIndex
|
||||||
|
while index < hexData.endIndex {
|
||||||
|
let nextIndex = hexData.index(index, offsetBy: 2, limitedBy: hexData.endIndex) ?? hexData.endIndex
|
||||||
|
let byteString = String(hexData[index..<nextIndex])
|
||||||
|
|
||||||
|
if let byte = UInt8(byteString, radix: 16) {
|
||||||
|
bytes.append(byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
index = nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return de(data: bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密数据
|
||||||
|
static func de(data: Data) -> Data {
|
||||||
|
guard !data.isEmpty else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
let saltLen = Int(data[data.startIndex])
|
||||||
|
guard data.count >= 1 + saltLen else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
let salt = data.subdata(in: 1..<1+saltLen)
|
||||||
|
let encryptedData = data.subdata(in: 1+saltLen..<data.count)
|
||||||
|
|
||||||
|
return deWithSalt(data: encryptedData, salt: salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用盐值解密数据
|
||||||
|
static func deWithSalt(data: Data, salt: Data) -> Data {
|
||||||
|
let decryptedData = cxEd(data: data)
|
||||||
|
return removeSalt(data: decryptedData, salt: salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密/解密数据(按位取反)
|
||||||
|
static func cxEd(data: Data) -> Data {
|
||||||
|
return Data(data.map { $0 ^ 0xFF })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据中移除盐值
|
||||||
|
static func removeSalt(data: Data, salt: Data) -> Data {
|
||||||
|
guard !salt.isEmpty else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = Data()
|
||||||
|
let saltBytes = [UInt8](salt)
|
||||||
|
let saltCount = saltBytes.count
|
||||||
|
|
||||||
|
for (index, byte) in data.enumerated() {
|
||||||
|
let saltByte = saltBytes[index % saltCount]
|
||||||
|
let decryptedByte = calRemoveSalt(v: byte, s: saltByte)
|
||||||
|
result.append(decryptedByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算移除盐值后的字节
|
||||||
|
static func calRemoveSalt(v: UInt8, s: UInt8) -> UInt8 {
|
||||||
|
if v >= s {
|
||||||
|
return v - s
|
||||||
|
} else {
|
||||||
|
return UInt8(0xFF) - (s - v) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
232
Fableon/Base/Request/FANetworkManager.swift
Normal file
232
Fableon/Base/Request/FANetworkManager.swift
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
//
|
||||||
|
// FANetworkManager.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Alamofire
|
||||||
|
import AdSupport
|
||||||
|
import SmartCodable
|
||||||
|
import YYCategories
|
||||||
|
|
||||||
|
/// 网络请求封装
|
||||||
|
class FANetworkManager {
|
||||||
|
|
||||||
|
static let manager = FANetworkManager()
|
||||||
|
|
||||||
|
private let operationQueue = OperationQueue()
|
||||||
|
private var tokenOperation: BlockOperation?
|
||||||
|
|
||||||
|
// 通用请求方法
|
||||||
|
func request<T: SmartCodable>(
|
||||||
|
_ url: String,
|
||||||
|
method: HTTPMethod = .post,
|
||||||
|
parameters: Parameters? = nil,
|
||||||
|
isLoding: Bool = false,
|
||||||
|
isToast: Bool = true,
|
||||||
|
completion: ((_ response: FANetworkManager.Response<T>) -> Void)?
|
||||||
|
) {
|
||||||
|
if FALogin.manager.token == nil, !isTokenUrl(url) {
|
||||||
|
self.requestUserToken(completer: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let _ = self.tokenOperation, !isTokenUrl(url) {
|
||||||
|
requestAddQueue(url, method: method, parameters: parameters, isLoding: isLoding, isToast: isToast, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_request(url, method: method, parameters: parameters, isLoding: isLoding, isToast: isToast, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func _request<T: SmartCodable>(
|
||||||
|
_ url: String,
|
||||||
|
method: HTTPMethod = .get,
|
||||||
|
parameters: Parameters? = nil,
|
||||||
|
isLoding: Bool,
|
||||||
|
isToast: Bool,
|
||||||
|
completion: ((_ response: FANetworkManager.Response<T>) -> Void)?
|
||||||
|
) {
|
||||||
|
if isLoding {
|
||||||
|
FAHUD.show()
|
||||||
|
}
|
||||||
|
AF.request(
|
||||||
|
url,
|
||||||
|
method: method,
|
||||||
|
parameters: parameters,
|
||||||
|
encoding: method == .get ? URLEncoding.default : JSONEncoding.default,
|
||||||
|
headers: self.headers
|
||||||
|
)
|
||||||
|
.responseString(completionHandler: { response in
|
||||||
|
if isLoding {
|
||||||
|
FAHUD.dismiss()
|
||||||
|
}
|
||||||
|
let code = response.response?.statusCode
|
||||||
|
|
||||||
|
if code == 401 || code == 402 || code == 403 {
|
||||||
|
if self.isTokenUrl(url) {
|
||||||
|
var response = FANetworkManager.Response<T>()
|
||||||
|
response.code = -1
|
||||||
|
completion?(response)
|
||||||
|
} else {
|
||||||
|
self.requestUserToken {
|
||||||
|
if FALogin.manager.token != nil {
|
||||||
|
FALogin.manager.requestUserInfo(completer: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let _ = self.tokenOperation, !self.isTokenUrl(url) {
|
||||||
|
self.requestAddQueue(url, method: method, parameters: parameters, isLoding: isLoding, isToast: isToast, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
switch response.result {
|
||||||
|
case .success(let data):
|
||||||
|
let decrypted = FACryptorService.decrypt(data: data)
|
||||||
|
if let parameters = parameters {
|
||||||
|
debugLog(parameters)
|
||||||
|
}
|
||||||
|
debugLog(url)
|
||||||
|
debugLog(decrypted)
|
||||||
|
if let response = FANetworkManager.Response<T>.deserialize(from: decrypted) {
|
||||||
|
completion?(response)
|
||||||
|
} else {
|
||||||
|
if isToast {
|
||||||
|
FAToast.show(text: "Error".localized)
|
||||||
|
}
|
||||||
|
var res = FANetworkManager.Response<T>()
|
||||||
|
res.code = -1
|
||||||
|
res.msg = "解析错误"
|
||||||
|
completion?(res)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
if isToast {
|
||||||
|
FAToast.show(text: "network_error_01".localized)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = FANetworkManager.Response<T>()
|
||||||
|
res.code = error.responseCode
|
||||||
|
res.msg = error.localizedDescription
|
||||||
|
completion?(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FANetworkManager {
|
||||||
|
|
||||||
|
private func requestUserToken(completer: (() -> Void)?) {
|
||||||
|
guard self.tokenOperation == nil else {
|
||||||
|
completer?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.tokenOperation = BlockOperation(block: {
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
|
||||||
|
FALogin.manager.requestUserToken {
|
||||||
|
do { semaphore.signal() }
|
||||||
|
self.tokenOperation = nil
|
||||||
|
completer?()
|
||||||
|
}
|
||||||
|
semaphore.wait()
|
||||||
|
})
|
||||||
|
operationQueue.addOperation(self.tokenOperation!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestAddQueue<T: SmartCodable>(_ url: String,
|
||||||
|
method: HTTPMethod,
|
||||||
|
parameters: Parameters?,
|
||||||
|
isLoding: Bool,
|
||||||
|
isToast: Bool,
|
||||||
|
completion: ((_ response: FANetworkManager.Response<T>) -> Void)?) {
|
||||||
|
guard let tokenOperation = self.tokenOperation else { return }
|
||||||
|
|
||||||
|
let requestOperation = BlockOperation {
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
self._request(url, method: method, parameters: parameters, isLoding: isLoding, isToast: isToast) { response in
|
||||||
|
semaphore.signal()
|
||||||
|
completion?(response)
|
||||||
|
}
|
||||||
|
semaphore.wait()
|
||||||
|
}
|
||||||
|
///设置依赖关系
|
||||||
|
requestOperation.addDependency(tokenOperation)
|
||||||
|
|
||||||
|
operationQueue.addOperation(requestOperation)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FANetworkManager {
|
||||||
|
|
||||||
|
private func isTokenUrl(_ url: String) -> Bool {
|
||||||
|
return url.contains("/customer/register")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var headers: HTTPHeaders {
|
||||||
|
let token = FALogin.manager.token?.token ?? ""
|
||||||
|
let dic = [
|
||||||
|
"authorization" : token,
|
||||||
|
"system-version" : UIDevice.current.systemVersion,
|
||||||
|
"lang-key" : "en",
|
||||||
|
"time-zone" : String.timeZone(),
|
||||||
|
"app-version" : (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "",
|
||||||
|
"brand" : "apple", //品牌
|
||||||
|
"app-name" : "Fableon",
|
||||||
|
"system-type" : "ios",
|
||||||
|
"model" : UIDevice.current.machineModelName ?? "",
|
||||||
|
"idfa" : ASIdentifierManager.shared().advertisingIdentifier.uuidString,
|
||||||
|
"device-id" : FADeviceIDManager.shared.id, //设备id
|
||||||
|
"device-gaid" : UIDevice.current.identifierForVendor?.uuidString ?? ""
|
||||||
|
]
|
||||||
|
return HTTPHeaders(dic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension FANetworkManager {
|
||||||
|
|
||||||
|
struct Response<T : SmartCodable>: SmartCodable {
|
||||||
|
|
||||||
|
var code: Int?
|
||||||
|
var data: T?
|
||||||
|
var msg: String?
|
||||||
|
|
||||||
|
var isSuccess: Bool {
|
||||||
|
return code == 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct List<T: SmartCodable>: SmartCodable {
|
||||||
|
var list: [T]?
|
||||||
|
var pagination: Pagination?
|
||||||
|
|
||||||
|
var hasNextPage: Bool {
|
||||||
|
let totalPage = pagination?.page_total ?? 0
|
||||||
|
let currentPage = pagination?.current_page ?? 0
|
||||||
|
return totalPage > currentPage
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pagination: SmartCodable {
|
||||||
|
var current_page: Int?
|
||||||
|
var page_size: Int?
|
||||||
|
var page_total: Int?
|
||||||
|
var total_size: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
73
Fableon/Base/Request/FANetworkMonitor.swift
Normal file
73
Fableon/Base/Request/FANetworkMonitor.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// FANetworkMonitor.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Network
|
||||||
|
|
||||||
|
class FANetworkMonitor {
|
||||||
|
static let manager = FANetworkMonitor()
|
||||||
|
|
||||||
|
///是否有网
|
||||||
|
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: FANetworkMonitor.networkStatusDidChangeNotification, object: nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.isReachable = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.isReachable == true {
|
||||||
|
self.isReachable = false
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: FANetworkMonitor.networkStatusDidChangeNotification, object: nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.isReachable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
monitor.start(queue: queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopMonitoring() {
|
||||||
|
monitor.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FANetworkMonitor {
|
||||||
|
///网络发生变化
|
||||||
|
@objc static let networkStatusDidChangeNotification = NSNotification.Name(rawValue: "FANetworkMonitor.networkStatusDidChangeNotification")
|
||||||
|
}
|
||||||
22
Fableon/Base/View/FACollectionView.swift
Normal file
22
Fableon/Base/View/FACollectionView.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// FACollectionView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FACollectionView: 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
101
Fableon/Base/View/FAImageView.swift
Normal file
101
Fableon/Base/View/FAImageView.swift
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// FAImageView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
@IBDesignable
|
||||||
|
class FAImageView: UIImageView {
|
||||||
|
|
||||||
|
var placeholderColor = UIColor._8_B_8_B_8_B
|
||||||
|
var placeholderImage = UIImage(named: "placeholder_image")
|
||||||
|
|
||||||
|
private lazy var placeholderImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView(image: placeholderImage)
|
||||||
|
imageView.isHidden = true
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(image: UIImage?) {
|
||||||
|
super.init(image: image)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(image: UIImage?, highlightedImage: UIImage?) {
|
||||||
|
super.init(image: image, highlightedImage: highlightedImage)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func _init() {
|
||||||
|
self.contentMode = .scaleAspectFill
|
||||||
|
self.layer.masksToBounds = true
|
||||||
|
if image == nil {
|
||||||
|
self.backgroundColor = self.placeholderColor
|
||||||
|
placeholderImageView.isHidden = false
|
||||||
|
}
|
||||||
|
addSubview(placeholderImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var image: UIImage? {
|
||||||
|
didSet {
|
||||||
|
if self.backgroundColor == nil && image == nil {
|
||||||
|
self.backgroundColor = self.placeholderColor
|
||||||
|
} else if image != nil {
|
||||||
|
if self.backgroundColor == self.placeholderColor {
|
||||||
|
self.backgroundColor = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if image == nil {
|
||||||
|
placeholderImageView.isHidden = false
|
||||||
|
} else {
|
||||||
|
placeholderImageView.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
placeholderImageView.frame = .init(x: 0, y: 0, width: self.bounds.width * (2 / 3), height: self.bounds.height * (2 / 3))
|
||||||
|
placeholderImageView.center = .init(x: self.bounds.width / 2, y: self.bounds.height / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIImageView {
|
||||||
|
func fa_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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
78
Fableon/Base/View/FAPanModalContentView.swift
Normal file
78
Fableon/Base/View/FAPanModalContentView.swift
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// FAPanModalContentView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import HWPanModal
|
||||||
|
|
||||||
|
class FAPanModalContentView: HWPanModalContentView {
|
||||||
|
|
||||||
|
|
||||||
|
var contentHeight = UIScreen.height * (2 / 3)
|
||||||
|
|
||||||
|
var mainScrollView: UIScrollView?
|
||||||
|
|
||||||
|
///更新UI contentSize发生变化时调用
|
||||||
|
func setNeedsLayoutUpdate() {
|
||||||
|
self.panModalSetNeedsLayoutUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.backgroundColor = ._000000.withAlphaComponent(0.5)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 24
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
Fableon/Base/View/FAScrollView.swift
Normal file
21
Fableon/Base/View/FAScrollView.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// FAScrollView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAScrollView: UIScrollView {
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
self.contentInsetAdjustmentBehavior = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
Fableon/Base/View/FATableView.swift
Normal file
49
Fableon/Base/View/FATableView.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// FATableView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FATableView: UITableView {
|
||||||
|
|
||||||
|
var insetGroupedMargins: CGFloat = 15
|
||||||
|
|
||||||
|
override init(frame: CGRect, style: UITableView.Style) {
|
||||||
|
super.init(frame: frame, style: style)
|
||||||
|
separatorColor = .FFFFFF.withAlphaComponent(0.4)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
47
Fableon/Base/View/FATableViewCell.swift
Normal file
47
Fableon/Base/View/FATableViewCell.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// FATableViewCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FATableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||||
|
super.setSelected(selected, animated: animated)
|
||||||
|
|
||||||
|
// Configure the view for the selected state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
self.layer.rasterizationScale = UIScreen.main.scale
|
||||||
|
self.layer.shouldRasterize = true
|
||||||
|
self.selectionStyle = .none
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension UITableViewCell {
|
||||||
|
|
||||||
|
var fa_tableView: UITableView? {
|
||||||
|
return self.value(forKey: "_tableView") as? UITableView
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Fableon/Base/WebView/FAAppWebViewController.swift
Normal file
68
Fableon/Base/WebView/FAAppWebViewController.swift
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// FAAppWebViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAAppWebViewController: FABaseWebViewController {
|
||||||
|
|
||||||
|
|
||||||
|
var id: String?
|
||||||
|
|
||||||
|
private var receiveDataCount = 0
|
||||||
|
|
||||||
|
var theme: String? = "theme_2"
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
// Do any additional setup after loading the view.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func fa_webViewDidFinishLoad(_ webView: FAWebView) {
|
||||||
|
super.fa_webViewDidFinishLoad(webView)
|
||||||
|
receiveDataCount = 0
|
||||||
|
receiveDataFromNative()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAAppWebViewController {
|
||||||
|
|
||||||
|
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" : FALogin.manager.token?.token ?? "",
|
||||||
|
"time_zone" : String.timeZone(),
|
||||||
|
"lang" : FALocalized.manager.currentLocalizedKey,
|
||||||
|
"type" : "ios",
|
||||||
|
]
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Fableon/Base/WebView/FABaseWebViewController.swift
Normal file
112
Fableon/Base/WebView/FABaseWebViewController.swift
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// FABaseWebViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
class FABaseWebViewController: FAViewController {
|
||||||
|
|
||||||
|
var webUrl: String?
|
||||||
|
|
||||||
|
///自动设置标题
|
||||||
|
var autoTitle = true
|
||||||
|
|
||||||
|
var needAutoRefresh = true
|
||||||
|
|
||||||
|
private(set) lazy var webView: FAWebView = {
|
||||||
|
let controller = WKUserContentController()
|
||||||
|
|
||||||
|
let config = WKWebViewConfiguration()
|
||||||
|
config.userContentController = controller
|
||||||
|
config.preferences.javaScriptEnabled = true
|
||||||
|
/** 默认是不能通过JS自动打开窗口的,必须通过用户交互才能打开 */
|
||||||
|
config.preferences.javaScriptCanOpenWindowsAutomatically = true
|
||||||
|
let webView = FAWebView(frame: self.view.bounds, configuration: config)
|
||||||
|
webView.delegate = self
|
||||||
|
return webView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
// self.edgesForExtendedLayout = []
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
|
||||||
|
if let url = webUrl {
|
||||||
|
self.load(webUrl: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
|
self.fa_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 FABaseWebViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
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.equalToSuperview().offset(UIScreen.navBarHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: -------------- VPWebViewDelegate --------------
|
||||||
|
extension FABaseWebViewController: FAWebViewDelegate {
|
||||||
|
|
||||||
|
func fa_webView(_ webView: FAWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool {
|
||||||
|
self.webView.isHidden = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_webViewDidStartLoad(_ webView: FAWebView) {
|
||||||
|
FAHUD.show(containerView: self.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_webView(webView: FAWebView, didChangeTitle title: String) {
|
||||||
|
if autoTitle {
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_webViewDidFinishLoad(_ webView: FAWebView) {
|
||||||
|
self.webView.isHidden = false
|
||||||
|
FAHUD.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_webView(_ webView: FAWebView, didFailLoadWithError error: any Error) {
|
||||||
|
FAHUD.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fa_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
147
Fableon/Base/WebView/FAWebView.swift
Normal file
147
Fableon/Base/WebView/FAWebView.swift
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
//
|
||||||
|
// FAWebView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
@preconcurrency import WebKit
|
||||||
|
import YYText
|
||||||
|
|
||||||
|
@objc protocol FAWebViewDelegate: NSObjectProtocol {
|
||||||
|
|
||||||
|
@objc optional func fa_webView(_ webView: FAWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool
|
||||||
|
|
||||||
|
@objc optional func fa_webViewDidStartLoad(_ webView: FAWebView)
|
||||||
|
|
||||||
|
@objc optional func fa_webViewDidFinishLoad(_ webView: FAWebView)
|
||||||
|
|
||||||
|
@objc optional func fa_webView(_ webView: FAWebView, didFailLoadWithError error: Error)
|
||||||
|
|
||||||
|
///进度
|
||||||
|
@objc optional func fa_webView(webView: FAWebView, didChangeProgress progress: CGFloat)
|
||||||
|
///标题
|
||||||
|
@objc optional func fa_webView(webView: FAWebView, didChangeTitle title: String)
|
||||||
|
|
||||||
|
///web交互用
|
||||||
|
@objc optional func fa_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FAWebView: WKWebView {
|
||||||
|
|
||||||
|
weak var delegate: FAWebViewDelegate?
|
||||||
|
|
||||||
|
private(set) var scriptMessageHandlerArray: [String] = [
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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? FAWebView == self {
|
||||||
|
if keyPath == "estimatedProgress", let progress = change?[NSKeyValueChangeKey.newKey] as? CGFloat {
|
||||||
|
self.delegate?.fa_webView?(webView: self, didChangeProgress: progress)
|
||||||
|
} else if keyPath == "title", let title = change?[NSKeyValueChangeKey.newKey] as? String {
|
||||||
|
self.delegate?.fa_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 FAWebView: 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?.fa_webView?(self, shouldStartLoadWith: navigationAction) {
|
||||||
|
if result {
|
||||||
|
decisionHandler(.allow)
|
||||||
|
} else {
|
||||||
|
decisionHandler(.cancel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decisionHandler(.allow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||||
|
self.delegate?.fa_webViewDidStartLoad?(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||||
|
self.delegate?.fa_webViewDidFinishLoad?(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||||
|
self.delegate?.fa_webView?(self, didFailLoadWithError: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||||
|
self.delegate?.fa_webView?(self, didFailLoadWithError: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK:-------------- WKScriptMessageHandler --------------
|
||||||
|
extension FAWebView: WKScriptMessageHandler {
|
||||||
|
|
||||||
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
self.delegate?.fa_userContentController?(userContentController, didReceive: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
247
Fableon/Class/Home/C/FAHomeViewController.swift
Normal file
247
Fableon/Class/Home/C/FAHomeViewController.swift
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
//
|
||||||
|
// FAHomeViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
import SnapKit
|
||||||
|
|
||||||
|
class FAHomeViewController: FAViewController {
|
||||||
|
|
||||||
|
private var viewModel = FAHomeViewModel()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var cvLayout: FAWaterfallFlowLayout = {
|
||||||
|
let layout = FAWaterfallFlowLayout()
|
||||||
|
layout.delegate = self
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionView: FACollectionView = {
|
||||||
|
let view = FACollectionView(frame: .zero, collectionViewLayout: cvLayout)
|
||||||
|
view.delegate = self
|
||||||
|
view.dataSource = self
|
||||||
|
view.contentInset = .init(top: 20, left: 0, bottom: 10, right: 0)
|
||||||
|
view.register(FAHomeBannerContentCell.self, forCellWithReuseIdentifier: "FAHomeBannerCell")
|
||||||
|
view.register(FAHomeMustSeeContentCell.self, forCellWithReuseIdentifier: "FAHomeMustSeeContentCell")
|
||||||
|
view.register(FAHomeNewContentCell.self, forCellWithReuseIdentifier: "FAHomeNewContentCell")
|
||||||
|
view.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
|
||||||
|
view.register(UINib(nibName: "FAHomeRecommendedCell", bundle: nil), forCellWithReuseIdentifier: "FAHomeRecommendedCell")
|
||||||
|
view.register(UINib(nibName: "FAHomeSectionTitleView", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "FAHomeSectionTitleView")
|
||||||
|
view.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "footer")
|
||||||
|
view.fa_addRefreshHeader(insetTop: view.contentInset.top) { [weak self] in
|
||||||
|
self?.handleHeaderRefresh(nil)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleView: UIView = {
|
||||||
|
let view = UIImageView(image: UIImage(named: "Thalire"))
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var searchButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let vc = FASearchViewController()
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}))
|
||||||
|
button.setImage(UIImage(named: "首页搜索i_ic"), for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: FANetworkMonitor.networkStatusDidChangeNotification, object: nil)
|
||||||
|
fa_setupLayout()
|
||||||
|
|
||||||
|
self.viewModel.requestHomeData { [weak self] in
|
||||||
|
self?.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||||
|
self.viewModel.requestHomeData { [weak self] in
|
||||||
|
self?.collectionView.reloadData()
|
||||||
|
self?.collectionView.fa_endHeaderRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func networkStatusDidChangeNotification() {
|
||||||
|
if self.viewModel.dataArr.isEmpty, FANetworkMonitor.manager.isReachable == true {
|
||||||
|
self.viewModel.requestHomeData { [weak self] in
|
||||||
|
self?.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAHomeViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
view.addSubview(titleView)
|
||||||
|
view.addSubview(searchButton)
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
|
||||||
|
titleView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.centerY.equalTo(self.view.snp.top).offset(UIScreen.safeTop + (UIScreen.navBarHeight - UIScreen.safeTop) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchButton.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalTo(titleView)
|
||||||
|
make.right.equalToSuperview().offset(-16)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||||
|
extension FAHomeViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||||
|
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let item = self.viewModel.dataArr[indexPath.section]
|
||||||
|
|
||||||
|
if item.type == .banner {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FAHomeBannerCell", for: indexPath) as! FAHomeBannerContentCell
|
||||||
|
cell.moduleItem = item.data as? FAHomeModuleItem
|
||||||
|
cell.viewModel = self.viewModel
|
||||||
|
return cell
|
||||||
|
|
||||||
|
} else if item.type == .mustSee {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FAHomeMustSeeContentCell", for: indexPath) as! FAHomeMustSeeContentCell
|
||||||
|
cell.configure(self.viewModel)
|
||||||
|
return cell
|
||||||
|
|
||||||
|
} else if item.type == .new {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FAHomeNewContentCell", for: indexPath) as! FAHomeNewContentCell
|
||||||
|
cell.configure(viewModel)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FAHomeRecommendedCell", for: indexPath) as! FAHomeRecommendedCell
|
||||||
|
|
||||||
|
cell.model = (item.data as? FAHomeModuleItem)?.list[indexPath.row]
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
||||||
|
if kind == UICollectionView.elementKindSectionHeader {
|
||||||
|
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FAHomeSectionTitleView", for: indexPath)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "footer", for: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||||
|
return self.viewModel.dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
let item = self.viewModel.dataArr[section]
|
||||||
|
if item.type == .recommended {
|
||||||
|
return (item.data as? FAHomeModuleItem)?.list.count ?? 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
let item = self.viewModel.dataArr[indexPath.section]
|
||||||
|
guard item.type == .recommended else { return }
|
||||||
|
let model = (item.data as? FAHomeModuleItem)?.list[indexPath.row]
|
||||||
|
self.viewModel.pushPlayerDetail(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: FAWaterfallMutiSectionDelegate
|
||||||
|
extension FAHomeViewController: FAWaterfallMutiSectionDelegate {
|
||||||
|
|
||||||
|
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
|
||||||
|
let homeItem = self.viewModel.dataArr[indexPath.section]
|
||||||
|
if homeItem.type == .recommended, let model = (homeItem.data as? FAHomeModuleItem)?.list[indexPath.row] {
|
||||||
|
if model.cellHeight > 0 {
|
||||||
|
return model.cellHeight
|
||||||
|
} else {
|
||||||
|
let size = CGSize.init(width: floor((UIScreen.width - 32 - 13) / 2) - 24, height: 1000)
|
||||||
|
|
||||||
|
let height = 219 + 6 + 12 + (model.name?.size(.font(ofSize: 14, weight: .medium), size).height ?? 0)
|
||||||
|
|
||||||
|
model.cellHeight = height
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return homeItem.cellHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnNumber(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> Int {
|
||||||
|
let homeItem = self.viewModel.dataArr[section]
|
||||||
|
if homeItem.type == .recommended {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func referenceSizeForHeader(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGSize {
|
||||||
|
let item = self.viewModel.dataArr[section]
|
||||||
|
guard item.type == .recommended else { return .zero }
|
||||||
|
return .init(width: UIScreen.width, height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
func spacingWithLastSection(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||||
|
if section == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 25
|
||||||
|
}
|
||||||
|
|
||||||
|
func insetForSection(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> UIEdgeInsets {
|
||||||
|
let homeItem = self.viewModel.dataArr[section]
|
||||||
|
if homeItem.type == .recommended {
|
||||||
|
return .init(top: 0, left: 16, bottom: 0, right: 16)
|
||||||
|
} else {
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineSpacing(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||||
|
let homeItem = self.viewModel.dataArr[section]
|
||||||
|
if homeItem.type == .recommended {
|
||||||
|
return 12
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func interitemSpacing(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||||
|
let homeItem = self.viewModel.dataArr[section]
|
||||||
|
if homeItem.type == .recommended {
|
||||||
|
return 13
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
113
Fableon/Class/Home/C/FASearchViewController.swift
Normal file
113
Fableon/Class/Home/C/FASearchViewController.swift
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// FASearchViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchViewController: FAViewController {
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var viewModel: FASearchViewModel = FASearchViewModel()
|
||||||
|
|
||||||
|
private lazy var returnButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setImage(UIImage(named: "Frame 3011"), for: .normal)
|
||||||
|
button.addAction(UIAction(handler: { [weak self] _ in
|
||||||
|
self?.handleNavigationBack()
|
||||||
|
}), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var textView: FASearchInputView = {
|
||||||
|
let view = FASearchInputView()
|
||||||
|
view.didSearch = { [weak self] text in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.search(text)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var homeView: FASearchHomeView = {
|
||||||
|
let view = FASearchHomeView()
|
||||||
|
view.viewModel = viewModel
|
||||||
|
view.didSearch = { [weak self] text in
|
||||||
|
self?.textView.text = text
|
||||||
|
self?.search(text)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var resultView: FASearchResultView = {
|
||||||
|
let view = FASearchResultView()
|
||||||
|
view.viewModel = viewModel
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
textView.becomeFirstResponder()
|
||||||
|
|
||||||
|
// homeView.isHidden = true
|
||||||
|
resultView.isHidden = true
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
|
||||||
|
self.viewModel.requestSearchRecommendData(completer: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func search(_ text: String) {
|
||||||
|
if text.isEmpty {
|
||||||
|
homeView.isHidden = false
|
||||||
|
resultView.isHidden = true
|
||||||
|
} else {
|
||||||
|
homeView.isHidden = true
|
||||||
|
resultView.isHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resultView.search(text)
|
||||||
|
self.viewModel.addSearchRecord(text: text)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FASearchViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
view.addSubview(returnButton)
|
||||||
|
view.addSubview(textView)
|
||||||
|
view.addSubview(homeView)
|
||||||
|
view.addSubview(resultView)
|
||||||
|
|
||||||
|
returnButton.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.safeTop)
|
||||||
|
make.height.equalTo(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
textView.snp.makeConstraints { make in
|
||||||
|
make.left.equalTo(returnButton.snp.right).offset(10)
|
||||||
|
make.centerY.equalTo(returnButton)
|
||||||
|
make.right.equalToSuperview().offset(-16)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalTo(returnButton.snp.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalTo(homeView)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
Fableon/Class/Home/M/FAHomeItem.swift
Normal file
23
Fableon/Class/Home/M/FAHomeItem.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// FAHomeItem.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAHomeItem: NSObject, Identifiable {
|
||||||
|
enum ItemType {
|
||||||
|
case banner
|
||||||
|
case mustSee
|
||||||
|
case new
|
||||||
|
case recommended
|
||||||
|
}
|
||||||
|
|
||||||
|
var type: ItemType?
|
||||||
|
//FAHomeModuleItem [FAHomeModuleItem]
|
||||||
|
var data: Any?
|
||||||
|
|
||||||
|
var cellHeight: CGFloat = 0
|
||||||
|
}
|
||||||
53
Fableon/Class/Home/M/FAHomeModuleItem.swift
Normal file
53
Fableon/Class/Home/M/FAHomeModuleItem.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// FAHomeModuleItem.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class FAHomeModuleItem: NSObject, Identifiable, SmartCodable {
|
||||||
|
required override init() { }
|
||||||
|
|
||||||
|
enum ModuleKey: String, SmartCaseDefaultable {
|
||||||
|
case banner = "home_banner"
|
||||||
|
case v3_recommand = "home_v3_recommand"
|
||||||
|
case new_recommand = "new_recommand"
|
||||||
|
///分类推荐
|
||||||
|
case cagetory_recommand = "home_cagetory_recommand"
|
||||||
|
case week_ranking = "week_ranking"
|
||||||
|
case week_recommend = "week_highest_recommend"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var module_key: ModuleKey?
|
||||||
|
var title: String?
|
||||||
|
var list: [FAShortPlayModel] = []
|
||||||
|
|
||||||
|
@SmartAny
|
||||||
|
var data: Any?
|
||||||
|
|
||||||
|
|
||||||
|
func didFinishMapping() {
|
||||||
|
if let data = data as? [[String : Any]] {
|
||||||
|
self.list = [FAShortPlayModel].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 = [FAShortPlayModel].deserialize(from: dataList) ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
43
Fableon/Class/Home/UI/FAHomeMustSeeContentView.swift
Normal file
43
Fableon/Class/Home/UI/FAHomeMustSeeContentView.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// FAHomeMustSeeContentView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FAHomeMustSeeContentView: View {
|
||||||
|
|
||||||
|
@Binding var list: [FAShortPlayModel]
|
||||||
|
|
||||||
|
@ObservedObject var viewModel: FAHomeViewModel
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
HStack {
|
||||||
|
Text("Editor's Picks".localized)
|
||||||
|
.font(Font.font(size: 16, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_FFFFFF))
|
||||||
|
.padding(.leading, 10)
|
||||||
|
.padding(.top, 12)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 11) {
|
||||||
|
ForEach(list.indices, id: \.self) { index in
|
||||||
|
if index < 2 {
|
||||||
|
FAHomeMustSeeShortView(model: $list[index], viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.frame(height: 310)
|
||||||
|
.background(Color(String.color_FFFFFF).opacity(0.2))
|
||||||
|
.cornerRadius(11)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
80
Fableon/Class/Home/UI/FAHomeMustSeeShortView.swift
Normal file
80
Fableon/Class/Home/UI/FAHomeMustSeeShortView.swift
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// FAHomeMustSeeShortView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
|
||||||
|
struct FAHomeMustSeeShortView: View {
|
||||||
|
|
||||||
|
@Binding var model: FAShortPlayModel
|
||||||
|
@ObservedObject var viewModel: FAHomeViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
|
ZStack() {
|
||||||
|
setupUI()
|
||||||
|
.onTapGesture {
|
||||||
|
self.viewModel.pushPlayerDetail(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private func setupUI() -> some View {
|
||||||
|
HStack() {
|
||||||
|
KFImage(URL(string: model.image_url ?? ""))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.cornerRadius(5)
|
||||||
|
.clipped()
|
||||||
|
.frame(width: 76, height: 102)
|
||||||
|
.padding(.leading, 9)
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(model.name ?? "")
|
||||||
|
.font(Font.font(size: 14, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_000000))
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image("Frame 2920")
|
||||||
|
|
||||||
|
Text("Watch".localized)
|
||||||
|
.font(Font.font(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_000000))
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding(.trailing, 3)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 18)
|
||||||
|
.background(content: {
|
||||||
|
LinearGradient(colors: [Color(String.color_BEDFFF), Color(String.color_52A2F1)], startPoint: .leading, endPoint: .trailing)
|
||||||
|
})
|
||||||
|
.cornerRadius(9)
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.trailing, 9)
|
||||||
|
.padding(.top, 9)
|
||||||
|
.padding(.bottom, 9)
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(height: 120)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color(String.color_DDEDFD).opacity(0.4))
|
||||||
|
.cornerRadius(7)
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 7)
|
||||||
|
.stroke(Color(String.color_A8DBFF), lineWidth: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
96
Fableon/Class/Home/UI/FAHomeMustSeeView.swift
Normal file
96
Fableon/Class/Home/UI/FAHomeMustSeeView.swift
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
//
|
||||||
|
// FAHomeMustSeeView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FAHomeMustSeeView: View {
|
||||||
|
|
||||||
|
|
||||||
|
@State var selected = 0
|
||||||
|
// @Binding var selected: Binding<Int> = projectedValue
|
||||||
|
|
||||||
|
@ObservedObject var viewModel: FAHomeViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("Must-see TV series".localized)
|
||||||
|
.font(Font.font(size: 18, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_FFFFFF))
|
||||||
|
.padding(.leading, 16)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Spacer(minLength: 12)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
menuView()
|
||||||
|
|
||||||
|
Spacer(minLength: 11)
|
||||||
|
|
||||||
|
contentView(index: selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func menuView() -> some View {
|
||||||
|
let list = viewModel.mustSeeArr
|
||||||
|
|
||||||
|
return VStack(spacing: 1) {
|
||||||
|
ForEach(list.indices, id: \.self) { index in
|
||||||
|
menuItemView(item: list[index], index: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.leading, 13)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func menuItemView(item: FAHomeModuleItem, index: Int) -> some View {
|
||||||
|
let isSelected = index == selected
|
||||||
|
let textColor = isSelected ? Color(String.color_FFFFFF) : Color(String.color_FFFFFF).opacity(0.8)
|
||||||
|
|
||||||
|
return VStack {
|
||||||
|
HStack {
|
||||||
|
Text(item.title ?? "")
|
||||||
|
.font(Font.font(size: 16, weight: .black))
|
||||||
|
.foregroundStyle(textColor)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("selection".localized)
|
||||||
|
.font(Font.font(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(textColor)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Image(isSelected ? "Frame 2914" : "Frame 2916")
|
||||||
|
.padding(.leading, 8)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 105, height: 72)
|
||||||
|
.background(Color(String.color_FFFFFF).opacity(0.2))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.frame(width: 111, height: 78)
|
||||||
|
.gradientBorder(colors: isSelected ? [Color(String.color_81CAFF), Color(String.color_20A2FF)] : [Color.clear, Color.clear], lineWidth: 6, cornerRadius: 11, startPoint: .topTrailing, endPoint: .bottomLeading)
|
||||||
|
|
||||||
|
.onTapGesture {
|
||||||
|
self.selected = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contentView(index: Int) -> some View {
|
||||||
|
return FAHomeMustSeeContentView(list: $viewModel.mustSeeArr[index].list, viewModel: self.viewModel)
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#Preview {
|
||||||
|
// FAHomeMustSeeView()
|
||||||
|
//}
|
||||||
87
Fableon/Class/Home/UI/FAHomeNewView.swift
Normal file
87
Fableon/Class/Home/UI/FAHomeNewView.swift
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
//
|
||||||
|
// FAHomeNewView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/27.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
import FSPagerView
|
||||||
|
|
||||||
|
struct FAHomeNewView: View {
|
||||||
|
|
||||||
|
@ObservedObject var viewModel: FAHomeViewModel
|
||||||
|
|
||||||
|
@State private var transformer: FSPagerViewTransformer = {
|
||||||
|
let transformer = FSPagerViewTransformer(type: .overlap)
|
||||||
|
transformer.minimumScale = 0.9
|
||||||
|
transformer.minimumAlpha = 1
|
||||||
|
return transformer
|
||||||
|
}()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let list = viewModel.homeNewItem?.list ?? []
|
||||||
|
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
Text("New Releases".localized)
|
||||||
|
.font(.font(size: 18, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_FFFFFF))
|
||||||
|
.padding(.leading, 16)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
FSPagerSwiftUIView(list) { model in
|
||||||
|
pageContentView(model)
|
||||||
|
}
|
||||||
|
.transformer(
|
||||||
|
transformer
|
||||||
|
)
|
||||||
|
.isLoop(true)
|
||||||
|
.itemSize(.init(width: 235, height: 235))
|
||||||
|
.frame(height: 235)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func pageContentView(_ model: FAShortPlayModel) -> some View {
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
KFImage(URL(string: model.image_url ?? ""))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: proxy.size.width, height: proxy.size.height)
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image("Frame 2921")
|
||||||
|
|
||||||
|
Text("Watch".localized)
|
||||||
|
.font(.font(size: 14, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_000000))
|
||||||
|
}
|
||||||
|
.frame(height: 30)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background {
|
||||||
|
LinearGradient(colors: [Color(String.color_C7DEF5), Color(String.color_52A2F1)], startPoint: .leading, endPoint: .trailing)
|
||||||
|
}
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 15))
|
||||||
|
}
|
||||||
|
.padding(.trailing, 17)
|
||||||
|
.padding(.leading, 17)
|
||||||
|
.padding(.bottom, 15)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
.onTapGesture {
|
||||||
|
self.viewModel.pushPlayerDetail(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
54
Fableon/Class/Home/UI/FAHomeRecommendedItemView.swift
Normal file
54
Fableon/Class/Home/UI/FAHomeRecommendedItemView.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// FAHomeRecommendedItemView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct FAHomeRecommendedItemView: View {
|
||||||
|
|
||||||
|
@Binding var model: FAShortPlayModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
KFImage(URL(string: model.image_url ?? ""))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: proxy.size.width, height: proxy.size.height)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let str = model.category?.first {
|
||||||
|
ZStack {
|
||||||
|
Text(str)
|
||||||
|
.font(.font(size: 12, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_FFFFFF))
|
||||||
|
.frame(height: 24)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.background(Color(String.color_000000).opacity(0.75))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
.padding(.trailing, 6)
|
||||||
|
.padding(.top, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 219)
|
||||||
|
|
||||||
|
Text(model.name ?? "")
|
||||||
|
.font(.font(size: 14, weight: .medium))
|
||||||
|
.foregroundStyle(Color(String.color_FFFFFF))
|
||||||
|
.padding(.init(top: 6, leading: 12, bottom: 12, trailing: 12))
|
||||||
|
|
||||||
|
}
|
||||||
|
.background(Color(String.color_333333))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Fableon/Class/Home/V/FAHomeBannerCell.swift
Normal file
54
Fableon/Class/Home/V/FAHomeBannerCell.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// FAHomeBannerCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import FSPagerView
|
||||||
|
|
||||||
|
class FAHomeBannerCell: FSPagerViewCell {
|
||||||
|
|
||||||
|
var model: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
coverImageView.fa_setImage(model?.horizontally_img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var coverImageView: FAImageView = {
|
||||||
|
let imageView = FAImageView()
|
||||||
|
imageView.layer.cornerRadius = 12
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.contentView.layer.shadowColor = UIColor.white.cgColor
|
||||||
|
self.contentView.layer.shadowRadius = 5
|
||||||
|
self.contentView.layer.shadowOpacity = 0.75
|
||||||
|
self.contentView.layer.shadowOffset = .init(width: 0, height: 1)
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAHomeBannerCell {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
contentView.addSubview(coverImageView)
|
||||||
|
|
||||||
|
coverImageView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
89
Fableon/Class/Home/V/FAHomeBannerContentCell.swift
Normal file
89
Fableon/Class/Home/V/FAHomeBannerContentCell.swift
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// FAHomeBannerCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
import FSPagerView
|
||||||
|
|
||||||
|
class FAHomeBannerContentCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
var moduleItem: FAHomeModuleItem? {
|
||||||
|
didSet {
|
||||||
|
pagerView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
weak var viewModel: FAHomeViewModel?
|
||||||
|
|
||||||
|
private lazy var pagerView: FSPagerView = {
|
||||||
|
let transformer = FAPagerViewTransformer(type: .linear)
|
||||||
|
transformer.minimumAlpha = 1
|
||||||
|
transformer.minimumScale = 0.9
|
||||||
|
|
||||||
|
|
||||||
|
let view = FSPagerView()
|
||||||
|
view.itemSize = .init(width: 282, height: 146)
|
||||||
|
view.transformer = transformer
|
||||||
|
view.delegate = self
|
||||||
|
view.dataSource = self
|
||||||
|
view.isInfinite = true
|
||||||
|
view.interitemSpacing = 1
|
||||||
|
view.register(FAHomeBannerCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
let subviews = pagerView.subviews.first?.subviews
|
||||||
|
subviews?.forEach {
|
||||||
|
if let view = $0 as? UICollectionView{
|
||||||
|
view.layer.masksToBounds = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAHomeBannerContentCell {
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
contentView.addSubview(pagerView)
|
||||||
|
|
||||||
|
pagerView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: FSPagerViewDelegate FSPagerViewDataSource
|
||||||
|
extension FAHomeBannerContentCell: FSPagerViewDelegate, FSPagerViewDataSource {
|
||||||
|
func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell {
|
||||||
|
let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "cell", at: index) as! FAHomeBannerCell
|
||||||
|
cell.model = moduleItem?.list[index]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfItems(in pagerView: FSPagerView) -> Int {
|
||||||
|
return moduleItem?.list.count ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func pagerView(_ pagerView: FSPagerView, didSelectItemAt index: Int) {
|
||||||
|
let model = moduleItem?.list[index]
|
||||||
|
self.viewModel?.pushPlayerDetail(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
48
Fableon/Class/Home/V/FAHomeMustSeeContentCell.swift
Normal file
48
Fableon/Class/Home/V/FAHomeMustSeeContentCell.swift
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
// FAHomeMustSeeContentCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class FAHomeMustSeeContentCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
|
||||||
|
private var hostingVC: UIHostingController<FAHomeMustSeeView>?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func configure(_ viewModel: FAHomeViewModel) {
|
||||||
|
// self.contentConfiguration = UIHostingConfiguration(content: {
|
||||||
|
// FAHomeMustSeeView(viewModel: viewModel)
|
||||||
|
// })
|
||||||
|
// .margins(.all, 0)
|
||||||
|
|
||||||
|
let uiView = FAHomeMustSeeView(viewModel: viewModel)
|
||||||
|
if let hostingVC = hostingVC {
|
||||||
|
hostingVC.rootView = uiView
|
||||||
|
} else {
|
||||||
|
let hostingVC = UIHostingController(rootView: uiView)
|
||||||
|
hostingVC.view.backgroundColor = .clear
|
||||||
|
contentView.addSubview(hostingVC.view)
|
||||||
|
hostingVC.view.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
self.hostingVC = hostingVC
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
Fableon/Class/Home/V/FAHomeNewContentCell.swift
Normal file
35
Fableon/Class/Home/V/FAHomeNewContentCell.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// FAHomeNewContentCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class FAHomeNewContentCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
private var hostingVC: UIHostingController<FAHomeNewView>?
|
||||||
|
|
||||||
|
func configure(_ viewModel: FAHomeViewModel) {
|
||||||
|
// self.contentConfiguration = UIHostingConfiguration(content: {
|
||||||
|
// FAHomeNewView(viewModel: viewModel)
|
||||||
|
// })
|
||||||
|
// .margins(.all, 0)
|
||||||
|
|
||||||
|
let uiView = FAHomeNewView(viewModel: viewModel)
|
||||||
|
if let hostingVC = hostingVC {
|
||||||
|
hostingVC.rootView = uiView
|
||||||
|
} else {
|
||||||
|
let hostingVC = UIHostingController(rootView: uiView)
|
||||||
|
hostingVC.view.backgroundColor = .clear
|
||||||
|
contentView.addSubview(hostingVC.view)
|
||||||
|
hostingVC.view.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hostingVC = hostingVC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Fableon/Class/Home/V/FAHomeRecommendedCell.swift
Normal file
42
Fableon/Class/Home/V/FAHomeRecommendedCell.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// FAHomeRecommendedCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAHomeRecommendedCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
var model: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
coverImageView.fa_setImage(model?.image_url)
|
||||||
|
|
||||||
|
titleLabel.text = model?.name
|
||||||
|
|
||||||
|
if let text = model?.category?.first, !text.isEmpty {
|
||||||
|
markView.isHidden = false
|
||||||
|
markLabel.text = text
|
||||||
|
} else {
|
||||||
|
markView.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet weak var coverImageView: FAImageView!
|
||||||
|
|
||||||
|
@IBOutlet weak var titleLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var markView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet weak var markLabel: UILabel!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.layer.cornerRadius = 8
|
||||||
|
self.layer.masksToBounds = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
91
Fableon/Class/Home/V/FAHomeRecommendedCell.xib
Normal file
91
Fableon/Class/Home/V/FAHomeRecommendedCell.xib
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FAHomeRecommendedCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="260" height="349"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="260" height="349"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="AeS-tg-K1o" customClass="FAImageView" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="260" height="219"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="219" id="FwC-h7-eqn"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lOB-pc-VGc">
|
||||||
|
<rect key="frame" x="12" y="225" width="36" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qIU-LN-B3H">
|
||||||
|
<rect key="frame" x="199.33333333333334" y="6" width="54.666666666666657" height="24"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kb9-Mz-sBq">
|
||||||
|
<rect key="frame" x="12.000000000000002" y="5.0000000000000009" width="30.666666666666671" height="14.333333333333336"/>
|
||||||
|
<fontDescription key="fontDescription" name="HelveticaNeue-Medium" family="Helvetica Neue" pointSize="12"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="#000000_0.75"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="24" id="BIp-U6-N1h"/>
|
||||||
|
<constraint firstItem="Kb9-Mz-sBq" firstAttribute="leading" secondItem="qIU-LN-B3H" secondAttribute="leading" constant="12" id="ZHY-Wj-iEk"/>
|
||||||
|
<constraint firstItem="Kb9-Mz-sBq" firstAttribute="centerY" secondItem="qIU-LN-B3H" secondAttribute="centerY" id="b5T-nu-Z5k"/>
|
||||||
|
<constraint firstItem="Kb9-Mz-sBq" firstAttribute="centerX" secondItem="qIU-LN-B3H" secondAttribute="centerX" id="nWL-eG-xqM"/>
|
||||||
|
</constraints>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<integer key="value" value="12"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||||
|
<color key="backgroundColor" name="#333333"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="lOB-pc-VGc" firstAttribute="leading" secondItem="ZTg-uK-7eu" secondAttribute="leading" constant="12" id="9sz-zS-G6x"/>
|
||||||
|
<constraint firstItem="qIU-LN-B3H" firstAttribute="trailing" secondItem="AeS-tg-K1o" secondAttribute="trailing" constant="-6" id="NsV-Te-Bu7"/>
|
||||||
|
<constraint firstItem="AeS-tg-K1o" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="WxT-0p-Elc"/>
|
||||||
|
<constraint firstItem="AeS-tg-K1o" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="daR-2A-o1r"/>
|
||||||
|
<constraint firstItem="qIU-LN-B3H" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" constant="6" id="n3p-MC-Uts"/>
|
||||||
|
<constraint firstItem="lOB-pc-VGc" firstAttribute="top" secondItem="AeS-tg-K1o" secondAttribute="bottom" constant="6" id="s9L-SF-7ko"/>
|
||||||
|
<constraint firstItem="ZTg-uK-7eu" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="lOB-pc-VGc" secondAttribute="trailing" constant="12" id="tba-OX-t1D"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="AeS-tg-K1o" secondAttribute="trailing" id="u5d-kn-sVr"/>
|
||||||
|
</constraints>
|
||||||
|
<size key="customSize" width="260" height="349"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="coverImageView" destination="AeS-tg-K1o" id="PGl-C1-X0a"/>
|
||||||
|
<outlet property="markLabel" destination="Kb9-Mz-sBq" id="FsI-8Y-r07"/>
|
||||||
|
<outlet property="markView" destination="qIU-LN-B3H" id="BOY-Lj-gVw"/>
|
||||||
|
<outlet property="titleLabel" destination="lOB-pc-VGc" id="eaL-Yh-t05"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="299.23664122137404" y="146.83098591549296"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="#000000_0.75">
|
||||||
|
<color red="0.0" green="0.0" blue="0.0" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
<namedColor name="#333333">
|
||||||
|
<color red="0.20000000000000001" green="0.20000000000000001" blue="0.20000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
17
Fableon/Class/Home/V/FAHomeSectionTitleView.swift
Normal file
17
Fableon/Class/Home/V/FAHomeSectionTitleView.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// FAHomeSectionTitleView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAHomeSectionTitleView: UICollectionReusableView {
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
// Initialization code
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
Fableon/Class/Home/V/FAHomeSectionTitleView.xib
Normal file
38
Fableon/Class/Home/V/FAHomeSectionTitleView.xib
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="U6b-Vx-4bR">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="555" height="123"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Recommended for you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8Zn-D3-Qbm">
|
||||||
|
<rect key="frame" x="16" y="0.0" width="186.33333333333334" height="21"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="18"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="VXr-Tz-HHm"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="8Zn-D3-Qbm" firstAttribute="leading" secondItem="U6b-Vx-4bR" secondAttribute="leading" constant="16" id="EqE-jT-c4H"/>
|
||||||
|
<constraint firstAttribute="top" secondItem="8Zn-D3-Qbm" secondAttribute="top" id="WgU-9Y-14J"/>
|
||||||
|
</constraints>
|
||||||
|
<point key="canvasLocation" x="318.32061068702291" y="66.549295774647888"/>
|
||||||
|
</collectionReusableView>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
95
Fableon/Class/Home/V/FASearchHomeView.swift
Normal file
95
Fableon/Class/Home/V/FASearchHomeView.swift
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// FASearchHomeView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchHomeView: UIView {
|
||||||
|
|
||||||
|
weak var viewModel: FASearchViewModel? {
|
||||||
|
didSet {
|
||||||
|
viewModel?.addObserver(self, forKeyPath: "recommendData", context: nil)
|
||||||
|
viewModel?.addObserver(self, forKeyPath: "recordList", context: nil)
|
||||||
|
|
||||||
|
self.recommendView.dataArr = self.viewModel?.recommendData ?? []
|
||||||
|
self.recordView.dataArr = self.viewModel?.recordList ?? []
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var didSearch: ((_ text: String) -> Void)?
|
||||||
|
|
||||||
|
private lazy var stackView: UIStackView = {
|
||||||
|
let view = UIStackView()
|
||||||
|
view.spacing = 20
|
||||||
|
view.axis = .vertical
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var recordView: FASearchRecordView = {
|
||||||
|
let view = FASearchRecordView()
|
||||||
|
view.didSearch = { [weak self] text in
|
||||||
|
self?.didSearch?(text)
|
||||||
|
}
|
||||||
|
view.didDelete = { [weak self] in
|
||||||
|
self?.viewModel?.clearSearchRecord()
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var recommendView: FASearchRecommendView = {
|
||||||
|
let view = FASearchRecommendView()
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
updateLayout()
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "recommendData" {
|
||||||
|
self.recommendView.dataArr = self.viewModel?.recommendData ?? []
|
||||||
|
} else if keyPath == "recordList" {
|
||||||
|
self.recordView.dataArr = self.viewModel?.recordList ?? []
|
||||||
|
}
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout() {
|
||||||
|
stackView.fa_removeAllArrangedSubview()
|
||||||
|
|
||||||
|
if self.recordView.dataArr.count > 0 {
|
||||||
|
stackView.addArrangedSubview(recordView)
|
||||||
|
}
|
||||||
|
if self.recommendView.dataArr.count > 0 {
|
||||||
|
stackView.addArrangedSubview(self.recommendView)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FASearchHomeView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(stackView)
|
||||||
|
|
||||||
|
stackView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(20)
|
||||||
|
make.bottom.lessThanOrEqualToSuperview()
|
||||||
|
// make.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
108
Fableon/Class/Home/V/FASearchInputView.swift
Normal file
108
Fableon/Class/Home/V/FASearchInputView.swift
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
//
|
||||||
|
// FASearchInputView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchInputView: UIView {
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
return .init(width: UIScreen.width, height: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var didSearch: ((_ text: String) -> Void)?
|
||||||
|
|
||||||
|
var text: String? {
|
||||||
|
get {
|
||||||
|
return textField.text
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
textField.text = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var iconImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView(image: UIImage(named: "Search"))
|
||||||
|
imageView.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var textField: UITextField = {
|
||||||
|
let textField = UITextField(frame: .zero)
|
||||||
|
textField.tintColor = UIColor.FFFFFF
|
||||||
|
textField.delegate = self
|
||||||
|
textField.returnKeyType = .search
|
||||||
|
textField.font = .font(ofSize: 12, weight: .medium)
|
||||||
|
textField.textColor = .FFFFFF
|
||||||
|
textField.attributedPlaceholder = NSAttributedString(string: "Search".localized, attributes: [
|
||||||
|
.font : UIFont.font(ofSize: 12, weight: .medium),
|
||||||
|
.foregroundColor : UIColor.FFFFFF.withAlphaComponent(0.5)
|
||||||
|
])
|
||||||
|
return textField
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
self.layer.cornerRadius = 16
|
||||||
|
self.layer.masksToBounds = true
|
||||||
|
self.backgroundColor = .FFFFFF_0_25
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
override func becomeFirstResponder() -> Bool {
|
||||||
|
super.becomeFirstResponder()
|
||||||
|
return self.textField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
override func resignFirstResponder() -> Bool {
|
||||||
|
super.resignFirstResponder()
|
||||||
|
return self.textField.resignFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FASearchInputView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(iconImageView)
|
||||||
|
addSubview(textField)
|
||||||
|
|
||||||
|
iconImageView.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.left.equalToSuperview().offset(18)
|
||||||
|
}
|
||||||
|
|
||||||
|
textField.snp.makeConstraints { make in
|
||||||
|
make.top.bottom.equalToSuperview()
|
||||||
|
make.left.equalTo(iconImageView.snp.right).offset(6)
|
||||||
|
make.right.equalToSuperview().offset(-18)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UITextFieldDelegate
|
||||||
|
extension FASearchInputView: UITextFieldDelegate {
|
||||||
|
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
textField.resignFirstResponder()
|
||||||
|
if let text = textField.text {
|
||||||
|
self.didSearch?(text)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
55
Fableon/Class/Home/V/FASearchRecommendCell.swift
Normal file
55
Fableon/Class/Home/V/FASearchRecommendCell.swift
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// FASearchRecommendCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchRecommendCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
|
||||||
|
var model: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
coverImageView.fa_setImage(model?.image_url)
|
||||||
|
titleLabel.text = model?.name
|
||||||
|
countLabel.text = "\(model?.watch_total ?? 0)"
|
||||||
|
epLabel.text = "Ep.##".localizedReplace(text: "\(model?.episode_total ?? 0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var row: Int = 0 {
|
||||||
|
didSet {
|
||||||
|
let num = row + 1
|
||||||
|
numberLabel.text = "\(num)"
|
||||||
|
switch num {
|
||||||
|
case 1:
|
||||||
|
numberLabel.textColor = .F_94_F_7_F
|
||||||
|
case 2:
|
||||||
|
numberLabel.textColor = .FE_6_C_05
|
||||||
|
case 3:
|
||||||
|
numberLabel.textColor = .F_8_D_01_D
|
||||||
|
default:
|
||||||
|
numberLabel.textColor = .FFFFFF.withAlphaComponent(0.8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet weak var numberLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var titleLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var coverImageView: FAImageView!
|
||||||
|
|
||||||
|
@IBOutlet weak var countLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var epLabel: UILabel!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
116
Fableon/Class/Home/V/FASearchRecommendCell.xib
Normal file
116
Fableon/Class/Home/V/FASearchRecommendCell.xib
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FASearchRecommendCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="427" height="110"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="427" height="110"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SP2-Rv-8zV">
|
||||||
|
<rect key="frame" x="12.666666666666664" y="41.666666666666664" width="11" height="26.999999999999993"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4ku-8h-G6M" customClass="FAImageView" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="37" y="0.0" width="52" height="110"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="52" id="sIY-Wo-Q8H"/>
|
||||||
|
</constraints>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<integer key="value" value="6"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a1H-XH-Dta">
|
||||||
|
<rect key="frame" x="97" y="7" width="32" height="15"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mA2-30-PsL">
|
||||||
|
<rect key="frame" x="382.33333333333331" y="48" width="31.666666666666686" height="14.333333333333336"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Frame 2915" translatesAutoresizingMaskIntoConstraints="NO" id="xLY-pp-ng9">
|
||||||
|
<rect key="frame" x="368.33333333333331" y="49" width="12" height="12"/>
|
||||||
|
</imageView>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JDA-rr-yn7">
|
||||||
|
<rect key="frame" x="97" y="28" width="39.666666666666657" height="11"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XCJ-Au-4ij">
|
||||||
|
<rect key="frame" x="9.0000000000000018" y="0.66666666666666785" width="21.666666666666671" height="9.6666666666666661"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="8"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="#6D6D6D_0.4"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="XCJ-Au-4ij" firstAttribute="centerY" secondItem="JDA-rr-yn7" secondAttribute="centerY" id="5VP-tP-Y8Q"/>
|
||||||
|
<constraint firstAttribute="height" constant="11" id="W6s-IK-Ums"/>
|
||||||
|
<constraint firstItem="XCJ-Au-4ij" firstAttribute="centerX" secondItem="JDA-rr-yn7" secondAttribute="centerX" id="ZFH-uO-qZv"/>
|
||||||
|
<constraint firstItem="XCJ-Au-4ij" firstAttribute="leading" secondItem="JDA-rr-yn7" secondAttribute="leading" constant="9" id="eMJ-20-GjA"/>
|
||||||
|
</constraints>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<real key="value" value="5.5"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="a1H-XH-Dta" firstAttribute="leading" secondItem="4ku-8h-G6M" secondAttribute="trailing" constant="8" id="CWI-yx-YfW"/>
|
||||||
|
<constraint firstItem="4ku-8h-G6M" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="37" id="DJd-9S-vAe"/>
|
||||||
|
<constraint firstItem="SP2-Rv-8zV" firstAttribute="centerX" secondItem="ZTg-uK-7eu" secondAttribute="leading" constant="18" id="LNZ-57-juM"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="4ku-8h-G6M" secondAttribute="bottom" id="Luc-T1-KaD"/>
|
||||||
|
<constraint firstItem="ZTg-uK-7eu" firstAttribute="trailing" secondItem="mA2-30-PsL" secondAttribute="trailing" constant="13" id="Mgk-UZ-DNC"/>
|
||||||
|
<constraint firstItem="mA2-30-PsL" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="RPU-Dv-KCI"/>
|
||||||
|
<constraint firstItem="4ku-8h-G6M" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="Yxw-uT-vEd"/>
|
||||||
|
<constraint firstItem="SP2-Rv-8zV" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="Z4h-SD-e52"/>
|
||||||
|
<constraint firstItem="ZTg-uK-7eu" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="a1H-XH-Dta" secondAttribute="trailing" constant="90" id="dJ6-D7-bi9"/>
|
||||||
|
<constraint firstItem="JDA-rr-yn7" firstAttribute="top" secondItem="a1H-XH-Dta" secondAttribute="bottom" constant="6" id="jdM-iM-Lbd"/>
|
||||||
|
<constraint firstItem="a1H-XH-Dta" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" constant="7" id="kwi-ly-o9u"/>
|
||||||
|
<constraint firstItem="xLY-pp-ng9" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="tUT-pl-HIL"/>
|
||||||
|
<constraint firstItem="JDA-rr-yn7" firstAttribute="leading" secondItem="a1H-XH-Dta" secondAttribute="leading" id="upz-Ss-Uhk"/>
|
||||||
|
<constraint firstItem="mA2-30-PsL" firstAttribute="leading" secondItem="xLY-pp-ng9" secondAttribute="trailing" constant="2" id="xx6-ue-UaT"/>
|
||||||
|
</constraints>
|
||||||
|
<size key="customSize" width="427" height="110"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="countLabel" destination="mA2-30-PsL" id="Fzo-V8-t5Z"/>
|
||||||
|
<outlet property="coverImageView" destination="4ku-8h-G6M" id="llT-4Q-HA2"/>
|
||||||
|
<outlet property="epLabel" destination="XCJ-Au-4ij" id="Och-Zg-g7E"/>
|
||||||
|
<outlet property="numberLabel" destination="SP2-Rv-8zV" id="mA4-c8-7XC"/>
|
||||||
|
<outlet property="titleLabel" destination="a1H-XH-Dta" id="sTz-nT-rBP"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="426.71755725190837" y="62.676056338028175"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="Frame 2915" width="12" height="12"/>
|
||||||
|
<namedColor name="#6D6D6D_0.4">
|
||||||
|
<color red="0.42745098039215684" green="0.42745098039215684" blue="0.42745098039215684" alpha="0.40000000000000002" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
99
Fableon/Class/Home/V/FASearchRecommendView.swift
Normal file
99
Fableon/Class/Home/V/FASearchRecommendView.swift
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// FASearchRecommendView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchRecommendView: UIView {
|
||||||
|
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
return .init(width: UIScreen.width, height: UIScreen.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataArr: [FAShortPlayModel] = [] {
|
||||||
|
didSet {
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var bgView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = ._5_CA_8_FF_0_2
|
||||||
|
view.fa_setRoundedCorner(topLeft: 27, topRight: 27, bottomLeft: 0, bottomRight: 0)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.itemSize = .init(width: UIScreen.width - 32, height: 70)
|
||||||
|
layout.minimumInteritemSpacing = 15
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionView: FACollectionView = {
|
||||||
|
let collectionView = FACollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.showsVerticalScrollIndicator = false
|
||||||
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
|
collectionView.keyboardDismissMode = .onDrag
|
||||||
|
collectionView.contentInset = .init(top: 15, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
|
||||||
|
collectionView.register(UINib(nibName: "FASearchRecommendCell", bundle: nil), forCellWithReuseIdentifier: "cell")
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FASearchRecommendView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(bgView)
|
||||||
|
addSubview(collectionView)
|
||||||
|
|
||||||
|
bgView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.top.equalToSuperview()
|
||||||
|
make.bottom.equalToSuperview()
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalTo(bgView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||||
|
extension FASearchRecommendView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! FASearchRecommendCell
|
||||||
|
cell.row = indexPath.row
|
||||||
|
cell.model = dataArr[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
return dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
let model = dataArr[indexPath.row]
|
||||||
|
let vc = FAPlayerDetailViewController()
|
||||||
|
vc.shortPlayId = model.short_play_id
|
||||||
|
self.viewController?.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Fableon/Class/Home/V/FASearchRecordCell.swift
Normal file
26
Fableon/Class/Home/V/FASearchRecordCell.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// FASearchRecordCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchRecordCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
static let TextFont: UIFont = .font(ofSize: 12, weight: .regular)
|
||||||
|
|
||||||
|
|
||||||
|
@IBOutlet weak var textLabel: UILabel!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
contentView.layer.cornerRadius = 12
|
||||||
|
contentView.layer.masksToBounds = true
|
||||||
|
contentView.backgroundColor = .FFFFFF_0_25
|
||||||
|
|
||||||
|
textLabel.font = Self.TextFont
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
Fableon/Class/Home/V/FASearchRecordCell.xib
Normal file
46
Fableon/Class/Home/V/FASearchRecordCell.xib
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FASearchRecordCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="160" height="50"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="160" height="50"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kF0-y1-Mfb">
|
||||||
|
<rect key="frame" x="64.666666666666671" y="18" width="31" height="14.333333333333336"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="kF0-y1-Mfb" firstAttribute="centerX" secondItem="gTV-IL-0wX" secondAttribute="centerX" id="NtY-07-0jo"/>
|
||||||
|
<constraint firstItem="kF0-y1-Mfb" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="YPF-Or-2JY"/>
|
||||||
|
</constraints>
|
||||||
|
<size key="customSize" width="160" height="50"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="textLabel" destination="kF0-y1-Mfb" id="uDt-Vq-Q8O"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="222.90076335877862" y="41.549295774647888"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
135
Fableon/Class/Home/V/FASearchRecordView.swift
Normal file
135
Fableon/Class/Home/V/FASearchRecordView.swift
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
//
|
||||||
|
// FASearchRecordView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import collection_view_layouts
|
||||||
|
|
||||||
|
class FASearchRecordView: UIView {
|
||||||
|
|
||||||
|
var didSearch: ((_ text: String) -> Void)?
|
||||||
|
var didDelete: (() -> Void)?
|
||||||
|
|
||||||
|
var dataArr: [String] = [] {
|
||||||
|
didSet {
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 14, weight: .medium)
|
||||||
|
label.textColor = .FFFFFF
|
||||||
|
label.text = "Historical search".localized
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var deleteButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setImage(UIImage(named: "路径 264"), for: .normal)
|
||||||
|
button.addAction(UIAction(handler: { [weak self] _ in
|
||||||
|
self?.didDelete?()
|
||||||
|
}), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionViewLayout: TagsLayout = {
|
||||||
|
let layout = TagsLayout()
|
||||||
|
layout.delegate = self
|
||||||
|
layout.contentPadding = ItemsPadding(horizontal: 16, vertical: 0)
|
||||||
|
layout.cellsPadding = ItemsPadding(horizontal: 12, vertical: 12)
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionView: FACollectionView = {
|
||||||
|
let collectionView = FACollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
|
||||||
|
collectionView.register(UINib(nibName: "FASearchRecordCell", bundle: nil), forCellWithReuseIdentifier: "tagCell")
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.collectionView.removeObserver(self, forKeyPath: "contentSize")
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "contentSize" {
|
||||||
|
let height = self.collectionView.contentSize.height
|
||||||
|
debugLog(height)
|
||||||
|
self.collectionView.snp.updateConstraints { make in
|
||||||
|
make.height.equalTo(height + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FASearchRecordView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(deleteButton)
|
||||||
|
addSubview(collectionView)
|
||||||
|
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.centerY.equalTo(deleteButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteButton.snp.makeConstraints { make in
|
||||||
|
make.right.equalToSuperview().offset(-16)
|
||||||
|
make.top.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.top.equalToSuperview().offset(28)
|
||||||
|
make.left.equalToSuperview()
|
||||||
|
make.right.equalToSuperview()
|
||||||
|
make.bottom.equalToSuperview()
|
||||||
|
make.height.equalTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UICollectionViewDataSource UICollectionViewDataSource
|
||||||
|
extension FASearchRecordView: UICollectionViewDataSource, UICollectionViewDelegate {
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
return dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "tagCell", for: indexPath) as! FASearchRecordCell
|
||||||
|
cell.textLabel.text = dataArr[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
self.didSearch?(dataArr[indexPath.row])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: LayoutDelegate
|
||||||
|
extension FASearchRecordView: LayoutDelegate {
|
||||||
|
|
||||||
|
func cellSize(indexPath: IndexPath) -> CGSize {
|
||||||
|
let text = dataArr[indexPath.row]
|
||||||
|
let size = text.size(FASearchRecordCell.TextFont, .init(width: UIScreen.width, height: 24))
|
||||||
|
return .init(width: size.width + 24, height: 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Fableon/Class/Home/V/FASearchResultCell.swift
Normal file
52
Fableon/Class/Home/V/FASearchResultCell.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// FASearchResultCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchResultCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
|
||||||
|
static let CellWidth: CGFloat = floor((UIScreen.width - 32 - 13) / 2)
|
||||||
|
static let CoverHeight: CGFloat = 219 / 164 * CellWidth
|
||||||
|
static let TitleFont = UIFont.font(ofSize: 14, weight: .medium)
|
||||||
|
|
||||||
|
var model: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
titleLabel.text = model?.name
|
||||||
|
coverImageView.fa_setImage(model?.image_url)
|
||||||
|
|
||||||
|
if let category = model?.category?.first, !category.isEmpty {
|
||||||
|
categoryView.isHidden = false
|
||||||
|
categoryLabel.text = category
|
||||||
|
} else {
|
||||||
|
categoryView.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@IBOutlet weak var coverImageView: FAImageView!
|
||||||
|
|
||||||
|
@IBOutlet weak var titleLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var categoryView: UIView!
|
||||||
|
|
||||||
|
@IBOutlet weak var categoryLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var coverHeight: NSLayoutConstraint!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
titleLabel.font = Self.TitleFont
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
coverHeight.constant = Self.CoverHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
99
Fableon/Class/Home/V/FASearchResultCell.xib
Normal file
99
Fableon/Class/Home/V/FASearchResultCell.xib
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FASearchResultCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="220" height="329"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="220" height="329"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="jd9-a4-YlS" customClass="FAImageView" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="220" height="219"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="219" id="JOV-Y4-g8T"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mFm-6k-c62">
|
||||||
|
<rect key="frame" x="12.000000000000004" y="225" width="41.333333333333343" height="20.333333333333343"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bci-Nw-k2u">
|
||||||
|
<rect key="frame" x="158.33333333333334" y="6" width="55.666666666666657" height="24"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lBv-CL-84w">
|
||||||
|
<rect key="frame" x="12.000000000000002" y="5.0000000000000009" width="31.666666666666671" height="14.333333333333336"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="12"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" name="#000000_0.75"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="lBv-CL-84w" firstAttribute="centerX" secondItem="bci-Nw-k2u" secondAttribute="centerX" id="4Uh-bU-kpV"/>
|
||||||
|
<constraint firstItem="lBv-CL-84w" firstAttribute="leading" secondItem="bci-Nw-k2u" secondAttribute="leading" constant="12" id="EGX-Dv-EsG"/>
|
||||||
|
<constraint firstItem="lBv-CL-84w" firstAttribute="centerY" secondItem="bci-Nw-k2u" secondAttribute="centerY" id="IeT-ph-eV1"/>
|
||||||
|
<constraint firstAttribute="height" constant="24" id="aHJ-CK-ts4"/>
|
||||||
|
</constraints>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<integer key="value" value="12"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||||
|
<color key="backgroundColor" name="#333333"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="ZTg-uK-7eu" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="mFm-6k-c62" secondAttribute="trailing" constant="12" id="6J0-90-aiN"/>
|
||||||
|
<constraint firstItem="bci-Nw-k2u" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" constant="6" id="7Zr-mb-gWM"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="jd9-a4-YlS" secondAttribute="trailing" id="9f3-El-3XU"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="bci-Nw-k2u" secondAttribute="trailing" constant="6" id="Ioz-EJ-wBm"/>
|
||||||
|
<constraint firstItem="mFm-6k-c62" firstAttribute="leading" secondItem="ZTg-uK-7eu" secondAttribute="leading" constant="12" id="Ise-os-chG"/>
|
||||||
|
<constraint firstItem="jd9-a4-YlS" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="eIb-e6-A5Y"/>
|
||||||
|
<constraint firstItem="mFm-6k-c62" firstAttribute="top" secondItem="jd9-a4-YlS" secondAttribute="bottom" constant="6" id="iC8-gl-1nQ"/>
|
||||||
|
<constraint firstItem="jd9-a4-YlS" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="jfP-TQ-cdS"/>
|
||||||
|
</constraints>
|
||||||
|
<size key="customSize" width="220" height="329"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<integer key="value" value="8"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
<connections>
|
||||||
|
<outlet property="categoryLabel" destination="lBv-CL-84w" id="Ye2-5P-wYv"/>
|
||||||
|
<outlet property="categoryView" destination="bci-Nw-k2u" id="IYC-4D-cE6"/>
|
||||||
|
<outlet property="coverHeight" destination="JOV-Y4-g8T" id="2Dv-rt-jYE"/>
|
||||||
|
<outlet property="coverImageView" destination="jd9-a4-YlS" id="eWc-3U-ZDz"/>
|
||||||
|
<outlet property="titleLabel" destination="mFm-6k-c62" id="fWp-yq-Mqw"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="267.17557251908397" y="139.08450704225353"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="#000000_0.75">
|
||||||
|
<color red="0.0" green="0.0" blue="0.0" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
<namedColor name="#333333">
|
||||||
|
<color red="0.20000000000000001" green="0.20000000000000001" blue="0.20000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
113
Fableon/Class/Home/V/FASearchResultView.swift
Normal file
113
Fableon/Class/Home/V/FASearchResultView.swift
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// FASearchResultView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchResultView: UIView {
|
||||||
|
|
||||||
|
weak var viewModel: FASearchViewModel?
|
||||||
|
|
||||||
|
var dataArr: [FAShortPlayModel] = []
|
||||||
|
|
||||||
|
private lazy var collectionViewLayout: FAWaterfallFlowLayout = {
|
||||||
|
let layout = FAWaterfallFlowLayout()
|
||||||
|
layout.delegate = self
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionView: FACollectionView = {
|
||||||
|
let collectionView = FACollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.ly_emptyView = FAEmpty.fa_emptyView(image: UIImage(named: "__question"), title: "empty_title_01".localized)
|
||||||
|
collectionView.register(UINib(nibName: "FASearchResultCell", bundle: nil), forCellWithReuseIdentifier: "cell")
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func search(_ text: String) {
|
||||||
|
if text.isEmpty {
|
||||||
|
self.dataArr.removeAll()
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
FAAPI.requestSearch(text: text) { [weak self] list in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let list = list else { return }
|
||||||
|
self.dataArr = list
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FASearchResultView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(collectionView)
|
||||||
|
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||||
|
extension FASearchResultView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! FASearchResultCell
|
||||||
|
cell.model = dataArr[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
return dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
let model = dataArr[indexPath.row]
|
||||||
|
let vc = FAPlayerDetailViewController()
|
||||||
|
vc.shortPlayId = model.short_play_id
|
||||||
|
self.viewController?.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: FAWaterfallMutiSectionDelegate
|
||||||
|
extension FASearchResultView: FAWaterfallMutiSectionDelegate {
|
||||||
|
|
||||||
|
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
|
||||||
|
let text = dataArr[indexPath.row].name ?? ""
|
||||||
|
|
||||||
|
return FASearchResultCell.CoverHeight + text.size(FASearchResultCell.TitleFont, .init(width: FASearchResultCell.CellWidth - 24, height: CGFloat(MAXFLOAT))).height + 18
|
||||||
|
}
|
||||||
|
|
||||||
|
func interitemSpacing(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||||
|
return 13
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineSpacing(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
|
||||||
|
func insetForSection(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> UIEdgeInsets {
|
||||||
|
return .init(top: 0, left: 16, bottom: 0, right: 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
115
Fableon/Class/Home/VM/FAHomeViewModel.swift
Normal file
115
Fableon/Class/Home/VM/FAHomeViewModel.swift
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// FAHomeViewModel.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class FAHomeViewModel: ObservableObject {
|
||||||
|
|
||||||
|
@Published var dataArr: [FAHomeItem] = []
|
||||||
|
|
||||||
|
@Published var bannerItem: FAHomeModuleItem?
|
||||||
|
@Published var mustSeeArr: [FAHomeModuleItem] = []
|
||||||
|
@Published var homeNewItem: FAHomeModuleItem?
|
||||||
|
@Published var recommendedItem: FAHomeModuleItem?
|
||||||
|
|
||||||
|
func requestHomeData(completer: (() -> Void)?) {
|
||||||
|
FAAPI.requestHomeModulesData { [weak self] list in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let list = list else {
|
||||||
|
completer?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.dataArr.removeAll()
|
||||||
|
|
||||||
|
var popularItem: FAHomeModuleItem?
|
||||||
|
var rankingsItem: FAHomeModuleItem?
|
||||||
|
var genresItem: FAHomeModuleItem?
|
||||||
|
var newItem: FAHomeModuleItem?
|
||||||
|
|
||||||
|
list.forEach {
|
||||||
|
if $0.module_key == .banner {
|
||||||
|
self.bannerItem = $0
|
||||||
|
} else if $0.module_key == .v3_recommand {
|
||||||
|
$0.title = "Popular".localized
|
||||||
|
popularItem = $0
|
||||||
|
} else if $0.module_key == .week_ranking {
|
||||||
|
$0.title = "Rankings".localized
|
||||||
|
rankingsItem = $0
|
||||||
|
} else if $0.module_key == .cagetory_recommand, genresItem == nil {
|
||||||
|
$0.title = "Genres".localized
|
||||||
|
genresItem = $0
|
||||||
|
} else if $0.module_key == .new_recommand {
|
||||||
|
$0.title = "New".localized
|
||||||
|
newItem = $0
|
||||||
|
self.homeNewItem = $0
|
||||||
|
} else if $0.module_key == .week_recommend {
|
||||||
|
self.recommendedItem = $0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mustAee: [FAHomeModuleItem] = []
|
||||||
|
if let item = popularItem {
|
||||||
|
mustAee.append(item)
|
||||||
|
}
|
||||||
|
if let item = rankingsItem {
|
||||||
|
mustAee.append(item)
|
||||||
|
}
|
||||||
|
if let item = genresItem {
|
||||||
|
mustAee.append(item)
|
||||||
|
}
|
||||||
|
if let item = newItem {
|
||||||
|
mustAee.append(item)
|
||||||
|
}
|
||||||
|
self.mustSeeArr = mustAee
|
||||||
|
|
||||||
|
if let item = self.bannerItem {
|
||||||
|
let homeItem = FAHomeItem()
|
||||||
|
homeItem.type = .banner
|
||||||
|
homeItem.data = item
|
||||||
|
homeItem.cellHeight = 150
|
||||||
|
self.dataArr.append(homeItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.mustSeeArr.count > 0 {
|
||||||
|
let homeItem = FAHomeItem()
|
||||||
|
homeItem.type = .mustSee
|
||||||
|
homeItem.data = self.mustSeeArr
|
||||||
|
homeItem.cellHeight = 350
|
||||||
|
self.dataArr.append(homeItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let item = self.homeNewItem {
|
||||||
|
let homeItem = FAHomeItem()
|
||||||
|
homeItem.type = .new
|
||||||
|
homeItem.data = item
|
||||||
|
homeItem.cellHeight = 272
|
||||||
|
self.dataArr.append(homeItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let item = self.recommendedItem {
|
||||||
|
let homeItem = FAHomeItem()
|
||||||
|
homeItem.type = .recommended
|
||||||
|
homeItem.data = item
|
||||||
|
self.dataArr.append(homeItem)
|
||||||
|
}
|
||||||
|
completer?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAHomeViewModel {
|
||||||
|
|
||||||
|
func pushPlayerDetail(_ model: FAShortPlayModel?) {
|
||||||
|
guard let model = model else { return }
|
||||||
|
|
||||||
|
let vc = FAPlayerDetailViewController()
|
||||||
|
vc.shortPlayId = model.short_play_id
|
||||||
|
FATool.topViewController?.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
57
Fableon/Class/Home/VM/FASearchViewModel.swift
Normal file
57
Fableon/Class/Home/VM/FASearchViewModel.swift
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// FASearchViewModel.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASearchViewModel: NSObject {
|
||||||
|
static let searchRecordUserDefaultKey = "FASearchViewModel.searchRecordUserDefaultKey"
|
||||||
|
|
||||||
|
@objc dynamic private(set) var recordList: [String] = (UserDefaults.standard.object(forKey: FASearchViewModel.searchRecordUserDefaultKey) as? [String]) ?? []
|
||||||
|
|
||||||
|
@objc dynamic private(set) lazy var recommendData: [FAShortPlayModel] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func addSearchRecord(text: String) {
|
||||||
|
guard !text.isEmpty else { return }
|
||||||
|
var list = recordList
|
||||||
|
|
||||||
|
for (index, value) in list.enumerated() {
|
||||||
|
if value == text {
|
||||||
|
list.remove(at: index)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.insert(text, at: 0)
|
||||||
|
|
||||||
|
if list.count > 10 {
|
||||||
|
list.removeLast()
|
||||||
|
}
|
||||||
|
recordList = list
|
||||||
|
|
||||||
|
UserDefaults.standard.set(list, forKey: FASearchViewModel.searchRecordUserDefaultKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearSearchRecord() {
|
||||||
|
recordList.removeAll()
|
||||||
|
UserDefaults.standard.set(recordList, forKey: FASearchViewModel.searchRecordUserDefaultKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
///获取推荐搜索
|
||||||
|
func requestSearchRecommendData(completer: (() -> Void)?) {
|
||||||
|
FAAPI.requestHotSearchData { [weak self] list in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let list = list {
|
||||||
|
self.recommendData = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
106
Fableon/Class/Me/C/FAAboutViewController.swift
Normal file
106
Fableon/Class/Me/C/FAAboutViewController.swift
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
//
|
||||||
|
// FAAboutViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAAboutViewController: FAViewController {
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var dataArr: [FAMeItemModel] = [
|
||||||
|
FAMeItemModel(type: .privacyPolicy, name: "Privacy Policy".localized),
|
||||||
|
FAMeItemModel(type: .userAgreement, name: "User Agreement".localized),
|
||||||
|
FAMeItemModel(type: .visitWebsite, name: "Visit Website".localized),
|
||||||
|
]
|
||||||
|
|
||||||
|
private lazy var tableView: FATableView = {
|
||||||
|
let tableView = FATableView(frame: .zero, style: .plain)
|
||||||
|
tableView.tableHeaderView = self.headerView
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.separatorInset = .init(top: 0, left: 32, bottom: 0, right: 32)
|
||||||
|
tableView.register(UINib(nibName: "FAAboutCell", bundle: nil), forCellReuseIdentifier: "cell")
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var headerView: FAAboutHeaderView = {
|
||||||
|
let view = FAAboutHeaderView(frame: .init(x: 0, y: 0, width: UIScreen.width, height: 186))
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.title = "About".localized
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
|
self.fa_setNavigationStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAAboutViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
view.addSubview(tableView)
|
||||||
|
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//MARK: UITableViewDelegate UITableViewDataSource
|
||||||
|
extension FAAboutViewController: UITableViewDelegate, UITableViewDataSource {
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! FAAboutCell
|
||||||
|
cell.item = dataArr[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
let item = dataArr[indexPath.row]
|
||||||
|
var urlStr: String? = nil
|
||||||
|
|
||||||
|
switch item.type {
|
||||||
|
case .privacyPolicy:
|
||||||
|
urlStr = FAWebBaseURL + "/private"
|
||||||
|
|
||||||
|
case .userAgreement:
|
||||||
|
urlStr = FAWebBaseURL + "/user_policy"
|
||||||
|
|
||||||
|
case .visitWebsite:
|
||||||
|
if let url = URL(string: FAWebBaseURL) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if let urlStr = urlStr {
|
||||||
|
let vc = FABaseWebViewController()
|
||||||
|
vc.webUrl = urlStr
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
30
Fableon/Class/Me/C/FAFeedbackViewController.swift
Normal file
30
Fableon/Class/Me/C/FAFeedbackViewController.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// FAFeedbackViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAFeedbackViewController: FAAppWebViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
self.webUrl = kFAFeedBackHomeWebUrl
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
// MARK: - Navigation
|
||||||
|
|
||||||
|
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||||
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||||
|
// Get the new view controller using segue.destination.
|
||||||
|
// Pass the selected object to the new view controller.
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
||||||
188
Fableon/Class/Me/C/FAMeViewController.swift
Normal file
188
Fableon/Class/Me/C/FAMeViewController.swift
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
//
|
||||||
|
// FAMeViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import YYText
|
||||||
|
|
||||||
|
class FAMeViewController: FAViewController {
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var dataArr: [FAMeItemModel] = {
|
||||||
|
let arr = [
|
||||||
|
// FAMeItemModel(type: .feedback, name: "Feedback".localized, icon: UIImage(named: "icon_feedback")),
|
||||||
|
FAMeItemModel(type: .about, name: "About".localized, icon: UIImage(named: "icon_about")),
|
||||||
|
// FAMeItemModel(type: .setting, name: "Setting".localized, icon: UIImage(named: "icon_setting"))
|
||||||
|
|
||||||
|
FAMeItemModel(type: .privacyPolicy, name: "Privacy Policy".localized, icon: UIImage(named: "icon_privacy")),
|
||||||
|
FAMeItemModel(type: .userAgreement, name: "User Agreement".localized, icon: UIImage(named: "icon_user")),
|
||||||
|
FAMeItemModel(type: .visitWebsite, name: "Visit Website".localized, icon: UIImage(named: "icon_visit")),
|
||||||
|
]
|
||||||
|
return arr
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var scrollView: FAScrollView = {
|
||||||
|
let scrollView = FAScrollView()
|
||||||
|
return scrollView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var headerView: FAMeHeaderView = {
|
||||||
|
let view = FAMeHeaderView()
|
||||||
|
view.userInfo = FALogin.manager.userInfo
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var contentView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = ._4_D_4_A_4_A.withAlphaComponent(0.5)
|
||||||
|
view.fa_setRoundedCorner(topLeft: 30, topRight: 30, bottomLeft: 0, bottomRight: 0)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var tableView: FATableView = {
|
||||||
|
let tableView = FATableView(frame: .zero, style: .plain)
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.dataSource = self
|
||||||
|
tableView.rowHeight = 56
|
||||||
|
tableView.separatorInset = .init(top: 0, left: 32, bottom: 0, right: 32)
|
||||||
|
tableView.register(UINib(nibName: "FAMeCell", bundle: nil), forCellReuseIdentifier: "cell")
|
||||||
|
tableView.addObserver(self, forKeyPath: "contentSize", context: nil)
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: FALogin.userInfoUpdateNotification, object: nil)
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
FALogin.manager.requestUserInfo(completer: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "contentSize" {
|
||||||
|
self.updateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@objc private func userInfoUpdateNotification() {
|
||||||
|
headerView.userInfo = FALogin.manager.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAMeViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
scrollView.addSubview(headerView)
|
||||||
|
scrollView.addSubview(contentView)
|
||||||
|
contentView.addSubview(tableView)
|
||||||
|
|
||||||
|
scrollView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.safeTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
headerView.snp.makeConstraints { make in
|
||||||
|
make.left.centerX.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(40)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.snp.makeConstraints { make in
|
||||||
|
make.left.centerX.equalToSuperview()
|
||||||
|
make.top.equalTo(headerView.snp.bottom).offset(24)
|
||||||
|
make.bottom.equalToSuperview()
|
||||||
|
make.height.equalTo(UIScreen.height - UIScreen.tabBarHeight - UIScreen.safeTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
tableView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(19)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLayout() {
|
||||||
|
let maxHeight = UIScreen.height - UIScreen.tabBarHeight - UIScreen.safeTop
|
||||||
|
let minHeight = UIScreen.height - UIScreen.tabBarHeight - UIScreen.safeTop - self.headerView.height - 40 - 24
|
||||||
|
var height = self.tableView.contentSize.height
|
||||||
|
if height > maxHeight {
|
||||||
|
height = maxHeight
|
||||||
|
}
|
||||||
|
if height < minHeight {
|
||||||
|
height = minHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.snp.updateConstraints { make in
|
||||||
|
make.height.equalTo(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UITableViewDelegate, UITableViewDataSource
|
||||||
|
extension FAMeViewController: UITableViewDelegate, UITableViewDataSource {
|
||||||
|
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! FAMeCell
|
||||||
|
cell.item = dataArr[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
let item = dataArr[indexPath.row]
|
||||||
|
switch item.type {
|
||||||
|
case .about:
|
||||||
|
let vc = FAAboutViewController()
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
|
||||||
|
case .setting:
|
||||||
|
let vc = FASettingViewController()
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
|
||||||
|
case .feedback:
|
||||||
|
let vc = FAFeedbackViewController()
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
|
||||||
|
case .privacyPolicy:
|
||||||
|
let vc = FABaseWebViewController()
|
||||||
|
vc.webUrl = FAWebBaseURL + "/private"
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
|
||||||
|
case .userAgreement:
|
||||||
|
let vc = FABaseWebViewController()
|
||||||
|
vc.webUrl = FAWebBaseURL + "/user_policy"
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
|
||||||
|
case .visitWebsite:
|
||||||
|
if let url = URL(string: FAWebBaseURL) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Fableon/Class/Me/C/FASettingViewController.swift
Normal file
26
Fableon/Class/Me/C/FASettingViewController.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// FASettingViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FASettingViewController: FAViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.title = "Settings".localized
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
|
self.fa_setNavigationStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
25
Fableon/Class/Me/M/FAMeItemModel.swift
Normal file
25
Fableon/Class/Me/M/FAMeItemModel.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// FAMeItemModel.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct FAMeItemModel {
|
||||||
|
|
||||||
|
enum ItemType {
|
||||||
|
case feedback
|
||||||
|
case about
|
||||||
|
case setting
|
||||||
|
case privacyPolicy
|
||||||
|
case userAgreement
|
||||||
|
case visitWebsite
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var type: ItemType?
|
||||||
|
var name: String?
|
||||||
|
var icon: UIImage?
|
||||||
|
}
|
||||||
33
Fableon/Class/Me/V/FAAboutCell.swift
Normal file
33
Fableon/Class/Me/V/FAAboutCell.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// FAAboutCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAAboutCell: FATableViewCell {
|
||||||
|
|
||||||
|
var item: FAMeItemModel? {
|
||||||
|
didSet {
|
||||||
|
titleLabel.text = item?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@IBOutlet weak var titleLabel: UILabel!
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
Fableon/Class/Me/V/FAAboutCell.xib
Normal file
46
Fableon/Class/Me/V/FAAboutCell.xib
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="FAAboutCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="V1G-kT-efB">
|
||||||
|
<rect key="frame" x="32" y="15" width="32.333333333333343" height="14.333333333333336"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="12"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="V1G-kT-efB" secondAttribute="trailing" constant="32" id="BIU-ey-6e6"/>
|
||||||
|
<constraint firstItem="V1G-kT-efB" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="cOi-y9-XWL"/>
|
||||||
|
<constraint firstItem="V1G-kT-efB" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="32" id="iB3-t3-yb3"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="titleLabel" destination="V1G-kT-efB" id="0yb-Q2-5y3"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="139" y="42"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
68
Fableon/Class/Me/V/FAAboutHeaderView.swift
Normal file
68
Fableon/Class/Me/V/FAAboutHeaderView.swift
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// FAAboutHeaderView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAAboutHeaderView: UIView {
|
||||||
|
|
||||||
|
private lazy var appLogoView: UIImageView = {
|
||||||
|
let imageView = UIImageView(image: UIImage(named: "logo_image_01"))
|
||||||
|
imageView.layer.cornerRadius = 8
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var nameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 18, weight: .bold)
|
||||||
|
label.textColor = .FFFFFF
|
||||||
|
label.text = kFAAPPName
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var versionLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 12, weight: .regular)
|
||||||
|
label.textColor = ._999999
|
||||||
|
label.text = "Version \(kFAAPPVersion)"
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAAboutHeaderView {
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(appLogoView)
|
||||||
|
addSubview(nameLabel)
|
||||||
|
addSubview(versionLabel)
|
||||||
|
|
||||||
|
appLogoView.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(30)
|
||||||
|
make.width.height.equalTo(84)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameLabel.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.top.equalTo(appLogoView.snp.bottom).offset(13)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionLabel.snp.makeConstraints { make in
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.top.equalTo(nameLabel.snp.bottom).offset(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Fableon/Class/Me/V/FAMeCell.swift
Normal file
36
Fableon/Class/Me/V/FAMeCell.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// FAMeCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAMeCell: FATableViewCell {
|
||||||
|
|
||||||
|
|
||||||
|
var item: FAMeItemModel? {
|
||||||
|
didSet {
|
||||||
|
iconImageView.image = item?.icon
|
||||||
|
nameLabel.text = item?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet weak var iconImageView: UIImageView!
|
||||||
|
|
||||||
|
@IBOutlet weak var nameLabel: UILabel!
|
||||||
|
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||||
|
super.setSelected(selected, animated: animated)
|
||||||
|
|
||||||
|
// Configure the view for the selected state
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
Fableon/Class/Me/V/FAMeCell.xib
Normal file
61
Fableon/Class/Me/V/FAMeCell.xib
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="67" id="KGk-i7-Jjw" customClass="FAMeCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="372" height="67"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="372" height="67"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="oa5-QI-16V">
|
||||||
|
<rect key="frame" x="32" y="23.666666666666671" width="20" height="20"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="20" id="6o1-we-ZGi"/>
|
||||||
|
<constraint firstAttribute="width" constant="20" id="qOw-IG-TUe"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DdC-ud-0R6">
|
||||||
|
<rect key="frame" x="64" y="25" width="36" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Frame 3015" translatesAutoresizingMaskIntoConstraints="NO" id="VZl-wc-TsY">
|
||||||
|
<rect key="frame" x="330" y="28.666666666666671" width="10" height="10"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="DdC-ud-0R6" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="AHI-cq-K4b"/>
|
||||||
|
<constraint firstItem="oa5-QI-16V" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="32" id="HgV-7h-PWs"/>
|
||||||
|
<constraint firstItem="DdC-ud-0R6" firstAttribute="leading" secondItem="oa5-QI-16V" secondAttribute="trailing" constant="12" id="JNq-Cy-RPf"/>
|
||||||
|
<constraint firstItem="oa5-QI-16V" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="OxB-Y8-ihy"/>
|
||||||
|
<constraint firstItem="VZl-wc-TsY" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="dcK-P6-Acp"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="VZl-wc-TsY" secondAttribute="trailing" constant="32" id="juM-rh-i2t"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="iconImageView" destination="oa5-QI-16V" id="65o-J4-4Pa"/>
|
||||||
|
<outlet property="nameLabel" destination="DdC-ud-0R6" id="yCn-Yd-29k"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="178.62595419847327" y="81.338028169014095"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="Frame 3015" width="10" height="10"/>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
78
Fableon/Class/Me/V/FAMeHeaderView.swift
Normal file
78
Fableon/Class/Me/V/FAMeHeaderView.swift
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// FAMeHeaderView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAMeHeaderView: UIView {
|
||||||
|
|
||||||
|
var userInfo: FAUserInfo? {
|
||||||
|
didSet {
|
||||||
|
avatarImageView.fa_setImage(userInfo?.avator)
|
||||||
|
|
||||||
|
userNameLabel.text = userInfo?.getNickName()
|
||||||
|
|
||||||
|
idLabel.text = "ID:\(userInfo?.customer_id ?? "")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var avatarImageView: FAImageView = {
|
||||||
|
let imageView = FAImageView()
|
||||||
|
imageView.layer.cornerRadius = 33
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var userNameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 14, weight: .medium)
|
||||||
|
label.textColor = .FFFFFF
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var idLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 12, weight: .regular)
|
||||||
|
label.textColor = .FFFFFF.withAlphaComponent(0.5)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAMeHeaderView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(avatarImageView)
|
||||||
|
addSubview(userNameLabel)
|
||||||
|
addSubview(idLabel)
|
||||||
|
|
||||||
|
avatarImageView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.top.equalToSuperview()
|
||||||
|
make.width.height.equalTo(66)
|
||||||
|
make.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
userNameLabel.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(avatarImageView).offset(15)
|
||||||
|
make.left.equalTo(avatarImageView.snp.right).offset(12)
|
||||||
|
make.right.lessThanOrEqualToSuperview().offset(-16)
|
||||||
|
}
|
||||||
|
|
||||||
|
idLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalTo(userNameLabel)
|
||||||
|
make.top.equalTo(userNameLabel.snp.bottom).offset(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
193
Fableon/Class/MyShort/C/FACollectViewController.swift
Normal file
193
Fableon/Class/MyShort/C/FACollectViewController.swift
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
//
|
||||||
|
// FACollectViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SnapKit
|
||||||
|
|
||||||
|
class FACollectViewController: FAViewController {
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var page = 1
|
||||||
|
private lazy var dataArr: [FAShortPlayModel] = []
|
||||||
|
|
||||||
|
private lazy var fa_isEditing = false {
|
||||||
|
didSet {
|
||||||
|
updateBarButton()
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
|
||||||
|
let width = floor((UIScreen.width - 16 - 32) / 3)
|
||||||
|
let height = 145 / 109 * width
|
||||||
|
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.itemSize = .init(width: width, height: height + 59)
|
||||||
|
layout.minimumLineSpacing = 12
|
||||||
|
layout.minimumInteritemSpacing = 8
|
||||||
|
layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16)
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionView: FACollectionView = {
|
||||||
|
let collectionView = FACollectionView(frame: .zero, collectionViewLayout: self.collectionViewLayout)
|
||||||
|
collectionView.contentInset = .init(top: 20, left: 0, bottom: 10, right: 0)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.ly_emptyView = FAEmpty.fa_emptyView(image: UIImage(named: "__shop-72"), title: "empty_title_02".localized)
|
||||||
|
collectionView.register(UINib(nibName: "FACollectCell", bundle: nil), forCellWithReuseIdentifier: "cell")
|
||||||
|
collectionView.fa_addRefreshHeader(insetTop: collectionView.contentInset.top) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.handleHeaderRefresh(nil)
|
||||||
|
}
|
||||||
|
collectionView.fa_addRefreshFooter(insetBottom: 0) { [weak self] in
|
||||||
|
self?.handleFooterRefresh(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var editBarButton: UIBarButtonItem = {
|
||||||
|
let item = UIBarButtonItem(image: UIImage(named: "编辑_icon"), style: .plain, target: self, action: #selector(handleEditButton))
|
||||||
|
return item
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var historyButton: UIBarButtonItem = {
|
||||||
|
let item = UIBarButtonItem(image: UIImage(named: "历史记录_icon"), style: .plain, target: self, action: #selector(handleHistoryButton))
|
||||||
|
return item
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var spaceButton: UIBarButtonItem = {
|
||||||
|
let item = UIBarButtonItem.fixedSpace(0)
|
||||||
|
return item
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.edgesForExtendedLayout = .top
|
||||||
|
self.title = "Collect".localized
|
||||||
|
|
||||||
|
|
||||||
|
self.navigationItem.rightBarButtonItems = [historyButton, spaceButton, editBarButton]
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
|
||||||
|
requestDataArr(page: 1, completer: nil)
|
||||||
|
|
||||||
|
updateBarButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
self.fa_isEditing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateBarButton() {
|
||||||
|
if fa_isEditing {
|
||||||
|
editBarButton.image = UIImage(named: "done")
|
||||||
|
} else {
|
||||||
|
editBarButton.image = UIImage(named: "编辑_icon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleEditButton() {
|
||||||
|
fa_isEditing = !fa_isEditing
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleHistoryButton() {
|
||||||
|
|
||||||
|
let vc = FAHistoryViewController()
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||||
|
self.requestDataArr(page: 1) { [weak self] in
|
||||||
|
self?.collectionView.fa_endHeaderRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handleFooterRefresh(_ completer: (() -> Void)?) {
|
||||||
|
self.requestDataArr(page: self.page + 1) { [weak self] in
|
||||||
|
self?.collectionView.fa_endFooterRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FACollectViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UICollectionViewDataSource UICollectionViewDelegate
|
||||||
|
extension FACollectViewController: UICollectionViewDataSource, UICollectionViewDelegate {
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! FACollectCell
|
||||||
|
cell.model = self.dataArr[indexPath.row]
|
||||||
|
cell.isEditing = fa_isEditing
|
||||||
|
cell.clickDeleteButton = { [weak self] cell in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let indexPath = self.collectionView.indexPath(for: cell) else { return }
|
||||||
|
guard let shortPlayId = cell.model?.short_play_id else { return }
|
||||||
|
|
||||||
|
FAAPI.requestShortCollect(isCollect: false, shortPlayId: shortPlayId, videoId: cell.model?.short_play_video_id) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.dataArr.remove(at: indexPath.row)
|
||||||
|
self.collectionView.deleteItems(at: [indexPath])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = FAPlayerDetailViewController()
|
||||||
|
vc.shortPlayId = model.short_play_id
|
||||||
|
FATool.topViewController?.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension FACollectViewController {
|
||||||
|
|
||||||
|
private func requestDataArr(page: Int, completer: (() -> Void)?) {
|
||||||
|
FAAPI.requestCollectList(page: page) { [weak self] listModel in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let list = listModel?.list {
|
||||||
|
if page == 1 {
|
||||||
|
self.dataArr.removeAll()
|
||||||
|
}
|
||||||
|
self.dataArr += list
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
self.page = page
|
||||||
|
}
|
||||||
|
completer?()
|
||||||
|
|
||||||
|
self.collectionView.fa_updateNoMoreDataState(listModel?.hasNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
Fableon/Class/MyShort/C/FAHistoryViewController.swift
Normal file
126
Fableon/Class/MyShort/C/FAHistoryViewController.swift
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
//
|
||||||
|
// FAHistoryViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAHistoryViewController: FAViewController {
|
||||||
|
|
||||||
|
|
||||||
|
private lazy var page = 1
|
||||||
|
private lazy var dataArr: [FAShortPlayModel] = []
|
||||||
|
|
||||||
|
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
|
||||||
|
let layout = UICollectionViewFlowLayout()
|
||||||
|
layout.minimumLineSpacing = 12
|
||||||
|
layout.itemSize = .init(width: UIScreen.width - 32, height: 115)
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionView: FACollectionView = {
|
||||||
|
let collectionView = FACollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.ly_emptyView = FAEmpty.fa_emptyView(image: UIImage(named: "__shop-72"), title: "empty_title_02".localized)
|
||||||
|
collectionView.contentInset = .init(top: 20, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
|
||||||
|
collectionView.register(UINib(nibName: "FAHistoryCell", bundle: nil), forCellWithReuseIdentifier: "cell")
|
||||||
|
collectionView.fa_addRefreshHeader(insetTop: collectionView.contentInset.top) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.handleHeaderRefresh(nil)
|
||||||
|
}
|
||||||
|
collectionView.fa_addRefreshFooter(insetBottom: 0) { [weak self] in
|
||||||
|
self?.handleFooterRefresh(nil)
|
||||||
|
}
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.title = "History".localized
|
||||||
|
self.edgesForExtendedLayout = .top
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
|
||||||
|
self.requestDataArr(page: 1, completer: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(false, animated: true)
|
||||||
|
fa_setNavigationStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
|
||||||
|
self.requestDataArr(page: 1) { [weak self] in
|
||||||
|
self?.collectionView.fa_endHeaderRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handleFooterRefresh(_ completer: (() -> Void)?) {
|
||||||
|
self.requestDataArr(page: self.page + 1) { [weak self] in
|
||||||
|
self?.collectionView.fa_endFooterRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAHistoryViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UICollectionViewDelegate UICollectionViewDataSource
|
||||||
|
extension FAHistoryViewController: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! FAHistoryCell
|
||||||
|
cell.model = dataArr[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
return self.dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
let model = dataArr[indexPath.row]
|
||||||
|
let vc = FAPlayerDetailViewController()
|
||||||
|
vc.shortPlayId = model.short_play_id
|
||||||
|
self.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAHistoryViewController {
|
||||||
|
|
||||||
|
private func requestDataArr(page: Int, completer: (() -> Void)?) {
|
||||||
|
|
||||||
|
FAAPI.requestPlayHistorys(page: page) { [weak self] listModel in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if let list = listModel?.list {
|
||||||
|
if page == 1 {
|
||||||
|
self.dataArr.removeAll()
|
||||||
|
}
|
||||||
|
self.dataArr += list
|
||||||
|
self.page = page
|
||||||
|
self.collectionView.reloadData()
|
||||||
|
}
|
||||||
|
completer?()
|
||||||
|
|
||||||
|
self.collectionView.fa_updateNoMoreDataState(listModel?.hasNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
51
Fableon/Class/MyShort/V/FACollectCell.swift
Normal file
51
Fableon/Class/MyShort/V/FACollectCell.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// FACollectCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FACollectCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
|
||||||
|
var clickDeleteButton: ((_ cell: FACollectCell) -> Void)?
|
||||||
|
|
||||||
|
var model: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
nameLabel.text = model?.name
|
||||||
|
coverImageView.fa_setImage(model?.image_url)
|
||||||
|
epLabel.text = "Ep.##".localizedReplace(text: "\(model?.episode_total ?? 0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEditing: Bool = true {
|
||||||
|
didSet {
|
||||||
|
deleteButton.isHidden = !isEditing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@IBOutlet weak var coverImageView: FAImageView!
|
||||||
|
|
||||||
|
@IBOutlet weak var nameLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var epLabel: UILabel!
|
||||||
|
|
||||||
|
@IBOutlet weak var deleteButton: UIButton!
|
||||||
|
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
self.deleteButton.layer.borderWidth = 1
|
||||||
|
self.deleteButton.layer.borderColor = UIColor.FFFFFF_0_25.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func handleDeleteButton(_ sender: Any) {
|
||||||
|
self.clickDeleteButton?(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
93
Fableon/Class/MyShort/V/FACollectCell.xib
Normal file
93
Fableon/Class/MyShort/V/FACollectCell.xib
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FACollectCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="236" height="329"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="236" height="329"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="ft8-51-pRf" customClass="FAImageView" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="236" height="270"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<integer key="value" value="6"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3hA-MZ-UZy">
|
||||||
|
<rect key="frame" x="0.0" y="277" width="31" height="14.333333333333314"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YRp-WJ-M8G">
|
||||||
|
<rect key="frame" x="0.0" y="294.33333333333331" width="26.333333333333332" height="12"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="10"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jTC-5t-LEB">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="236" height="270"/>
|
||||||
|
<color key="backgroundColor" name="#000000_0.75"/>
|
||||||
|
<state key="normal" title="Button"/>
|
||||||
|
<buttonConfiguration key="configuration" style="plain" image="删除"/>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<integer key="value" value="6"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
<userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
<connections>
|
||||||
|
<action selector="handleDeleteButton:" destination="gTV-IL-0wX" eventType="touchUpInside" id="qhI-tk-v9r"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="jTC-5t-LEB" firstAttribute="trailing" secondItem="ft8-51-pRf" secondAttribute="trailing" id="4mC-59-ddf"/>
|
||||||
|
<constraint firstItem="YRp-WJ-M8G" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="KxM-Ls-Nc8"/>
|
||||||
|
<constraint firstItem="jTC-5t-LEB" firstAttribute="bottom" secondItem="ft8-51-pRf" secondAttribute="bottom" id="LcN-MM-5FL"/>
|
||||||
|
<constraint firstItem="jTC-5t-LEB" firstAttribute="leading" secondItem="ft8-51-pRf" secondAttribute="leading" id="SDp-b4-9AN"/>
|
||||||
|
<constraint firstItem="ft8-51-pRf" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="WFp-eV-4ML"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="ft8-51-pRf" secondAttribute="bottom" constant="59" id="YFx-jl-R0t"/>
|
||||||
|
<constraint firstItem="ft8-51-pRf" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="crW-Gh-emI"/>
|
||||||
|
<constraint firstItem="3hA-MZ-UZy" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="hu5-S1-g5i"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="ft8-51-pRf" secondAttribute="trailing" id="i2G-DF-5DP"/>
|
||||||
|
<constraint firstItem="3hA-MZ-UZy" firstAttribute="top" secondItem="ft8-51-pRf" secondAttribute="bottom" constant="7" id="rAf-Lw-Ija"/>
|
||||||
|
<constraint firstItem="jTC-5t-LEB" firstAttribute="top" secondItem="ft8-51-pRf" secondAttribute="top" id="shQ-wC-7bx"/>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="3hA-MZ-UZy" secondAttribute="trailing" id="vN9-gX-ck4"/>
|
||||||
|
<constraint firstItem="YRp-WJ-M8G" firstAttribute="top" secondItem="3hA-MZ-UZy" secondAttribute="bottom" constant="3" id="yOT-VM-d0c"/>
|
||||||
|
</constraints>
|
||||||
|
<size key="customSize" width="236" height="329"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="coverImageView" destination="ft8-51-pRf" id="MLR-b4-bcQ"/>
|
||||||
|
<outlet property="deleteButton" destination="jTC-5t-LEB" id="0GS-YK-3Ag"/>
|
||||||
|
<outlet property="epLabel" destination="YRp-WJ-M8G" id="Isd-pp-hUI"/>
|
||||||
|
<outlet property="nameLabel" destination="3hA-MZ-UZy" id="62J-96-16m"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="279.38931297709922" y="139.78873239436621"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="删除" width="26" height="26"/>
|
||||||
|
<namedColor name="#000000_0.75">
|
||||||
|
<color red="0.0" green="0.0" blue="0.0" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
35
Fableon/Class/MyShort/V/FAHistoryCell.swift
Normal file
35
Fableon/Class/MyShort/V/FAHistoryCell.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// FAHistoryCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/13.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAHistoryCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
var model: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
coverImageView.fa_setImage(model?.image_url)
|
||||||
|
nameLabel.text = model?.name
|
||||||
|
epLabel.text = "Ep.##".localizedReplace(text: model?.current_episode) + "/" + "Ep.##".localizedReplace(text: "\(model?.episode_total ?? 0)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@IBOutlet weak var coverImageView: FAImageView!
|
||||||
|
|
||||||
|
@IBOutlet weak var nameLabel: UILabel!
|
||||||
|
|
||||||
|
|
||||||
|
@IBOutlet weak var epLabel: UILabel!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
self.contentView.layer.cornerRadius = 13
|
||||||
|
self.contentView.layer.masksToBounds = true
|
||||||
|
self.contentView.backgroundColor = ._5_CA_8_FF_0_2
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
77
Fableon/Class/MyShort/V/FAHistoryCell.xib
Normal file
77
Fableon/Class/MyShort/V/FAHistoryCell.xib
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FAHistoryCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="338" height="130"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="338" height="130"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bzV-5Y-YHI" customClass="FAImageView" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="12" y="12" width="68" height="106"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="68" id="MMl-Ix-A0Y"/>
|
||||||
|
</constraints>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||||
|
<integer key="value" value="5"/>
|
||||||
|
</userDefinedRuntimeAttribute>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
</imageView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Oh0-jp-gJm">
|
||||||
|
<rect key="frame" x="92" y="37.666666666666664" width="36" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YAk-yJ-afV">
|
||||||
|
<rect key="frame" x="92" y="62.666666666666657" width="26.333333333333329" height="12"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="10"/>
|
||||||
|
<color key="textColor" name="#FFFFFF"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Frame 3012" translatesAutoresizingMaskIntoConstraints="NO" id="xGT-Pb-RnF">
|
||||||
|
<rect key="frame" x="306" y="55" width="20" height="20"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Oh0-jp-gJm" secondAttribute="trailing" constant="90" id="8SF-0D-cdU"/>
|
||||||
|
<constraint firstItem="Oh0-jp-gJm" firstAttribute="leading" secondItem="bzV-5Y-YHI" secondAttribute="trailing" constant="12" id="HFd-H7-RQU"/>
|
||||||
|
<constraint firstItem="YAk-yJ-afV" firstAttribute="leading" secondItem="Oh0-jp-gJm" secondAttribute="leading" id="K3I-e4-hbk"/>
|
||||||
|
<constraint firstItem="bzV-5Y-YHI" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" constant="12" id="PZV-aq-N5S"/>
|
||||||
|
<constraint firstItem="bzV-5Y-YHI" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" constant="12" id="Ssy-Q9-jip"/>
|
||||||
|
<constraint firstItem="xGT-Pb-RnF" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="TzV-OM-veN"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="bzV-5Y-YHI" secondAttribute="bottom" constant="12" id="Wgj-z6-4a0"/>
|
||||||
|
<constraint firstItem="Oh0-jp-gJm" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="top" constant="46" id="kgG-BH-49a"/>
|
||||||
|
<constraint firstItem="xGT-Pb-RnF" firstAttribute="trailing" secondItem="ZTg-uK-7eu" secondAttribute="trailing" constant="-12" id="nL7-h7-dTk"/>
|
||||||
|
<constraint firstItem="YAk-yJ-afV" firstAttribute="top" secondItem="Oh0-jp-gJm" secondAttribute="bottom" constant="8" id="q73-qQ-lSH"/>
|
||||||
|
</constraints>
|
||||||
|
<size key="customSize" width="338" height="130"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="coverImageView" destination="bzV-5Y-YHI" id="3QE-jt-Kcw"/>
|
||||||
|
<outlet property="epLabel" destination="YAk-yJ-afV" id="Lyz-Jh-61u"/>
|
||||||
|
<outlet property="nameLabel" destination="Oh0-jp-gJm" id="mwy-Eq-RzR"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="358.77862595419845" y="69.718309859154928"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="Frame 3012" width="20" height="20"/>
|
||||||
|
<namedColor name="#FFFFFF">
|
||||||
|
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</namedColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
23
Fableon/Class/Player/M/FAShortDetailModel.swift
Normal file
23
Fableon/Class/Player/M/FAShortDetailModel.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// FAShortDetailModel.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
|
||||||
|
class FAShortDetailModel: NSObject, Identifiable, SmartCodable {
|
||||||
|
|
||||||
|
var video_info: FAVideoInfoModel?
|
||||||
|
var shortPlayInfo: FAShortPlayModel?
|
||||||
|
var episodeList: [FAVideoInfoModel]?
|
||||||
|
var is_collect: Bool?
|
||||||
|
var share_coin: Int?
|
||||||
|
|
||||||
|
required override init() { }
|
||||||
|
|
||||||
|
}
|
||||||
39
Fableon/Class/Player/M/FAShortPlayModel.swift
Normal file
39
Fableon/Class/Player/M/FAShortPlayModel.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// FAShortPlayModel.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class FAShortPlayModel: NSObject, Identifiable, SmartCodable {
|
||||||
|
required override init() { }
|
||||||
|
|
||||||
|
var id: String?
|
||||||
|
var fa_description: String?
|
||||||
|
var name: String?
|
||||||
|
var watch_total: Int?
|
||||||
|
var current_episode: String?
|
||||||
|
var image_url: String?
|
||||||
|
var is_collect: Bool?
|
||||||
|
var collect_total: Int?
|
||||||
|
var episode_total: Int?
|
||||||
|
var horizontally_img: String?
|
||||||
|
var category: [String]?
|
||||||
|
var short_play_id: String?
|
||||||
|
var short_play_video_id: String?
|
||||||
|
var video_info: FAVideoInfoModel?
|
||||||
|
|
||||||
|
@SmartIgnored
|
||||||
|
var cellHeight: CGFloat = 0
|
||||||
|
|
||||||
|
|
||||||
|
static func mappingForKey() -> [SmartKeyTransformer]? {
|
||||||
|
return [
|
||||||
|
CodingKeys.fa_description <--- ["description", "short_video_description"],
|
||||||
|
CodingKeys.name <--- ["short_video_title", "name"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Fableon/Class/Player/M/FAVideoInfoModel.swift
Normal file
25
Fableon/Class/Player/M/FAVideoInfoModel.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// FAVideoInfoModel.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SmartCodable
|
||||||
|
|
||||||
|
class FAVideoInfoModel: NSObject, Identifiable, SmartCodable {
|
||||||
|
|
||||||
|
required override init() { }
|
||||||
|
|
||||||
|
var short_play_id: String?
|
||||||
|
var short_play_video_id: String?
|
||||||
|
var video_url: String?
|
||||||
|
var episode: String?
|
||||||
|
var coins: Int?
|
||||||
|
///是否锁定,购买后解锁
|
||||||
|
var is_lock: Bool?
|
||||||
|
var image_url: String?
|
||||||
|
///播放进度,毫秒
|
||||||
|
var play_seconds: Int?
|
||||||
|
}
|
||||||
46
Fableon/Class/Player/UI/FAPlayerEpUIButton.swift
Normal file
46
Fableon/Class/Player/UI/FAPlayerEpUIButton.swift
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// FAPlayerEpUIButton.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/18.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FAPlayerEpUIButton: View {
|
||||||
|
|
||||||
|
var text: String?
|
||||||
|
|
||||||
|
var clickHandle: (() -> Void)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
|
||||||
|
HStack() {
|
||||||
|
Spacer(minLength: 14)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 9) {
|
||||||
|
Image("Frame 3008")
|
||||||
|
Text(text ?? "")
|
||||||
|
.font(Font.font(size: 12, weight: .regular))
|
||||||
|
.foregroundStyle(Color.FFFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
Image("Frame 3009")
|
||||||
|
}
|
||||||
|
Spacer(minLength: 14)
|
||||||
|
}
|
||||||
|
.frame(height: 32)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color.init(.color_FFFFFF).opacity(0.2))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||||
|
.onTapGesture {
|
||||||
|
self.clickHandle?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
FAPlayerEpUIButton()
|
||||||
|
}
|
||||||
187
Fableon/Class/Player/V/FAEpMenuView.swift
Normal file
187
Fableon/Class/Player/V/FAEpMenuView.swift
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
//
|
||||||
|
// FAEpMenuView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import YYCategories
|
||||||
|
|
||||||
|
class FAEpMenuView: UIView {
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
return CGSize(width: UIScreen.width, height: 35)
|
||||||
|
}
|
||||||
|
|
||||||
|
var didSelectedIndex: ((_ index: Int) -> Void)?
|
||||||
|
|
||||||
|
var dataArr: [String] = [] {
|
||||||
|
didSet {
|
||||||
|
self.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedIndex: Int = 0 {
|
||||||
|
didSet {
|
||||||
|
self.buttonArr.forEach {
|
||||||
|
$0.isSelected = $0.tag == selectedIndex
|
||||||
|
if $0.isSelected {
|
||||||
|
self.updateLinePosition(to: $0, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var buttonArr: [UIButton] = []
|
||||||
|
|
||||||
|
//MARK: UI属性
|
||||||
|
private lazy var scrollView: FAScrollView = {
|
||||||
|
let scrollView = FAScrollView()
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.showsHorizontalScrollIndicator = false
|
||||||
|
return scrollView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var lineView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.layer.cornerRadius = 2
|
||||||
|
view.layer.masksToBounds = true
|
||||||
|
view.backgroundColor = ._35_A_4_FE
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reloadData() {
|
||||||
|
buttonArr.forEach {
|
||||||
|
$0.removeFromSuperview()
|
||||||
|
}
|
||||||
|
buttonArr.removeAll()
|
||||||
|
|
||||||
|
let count = self.dataArr.count
|
||||||
|
|
||||||
|
var previousButton: UIButton?
|
||||||
|
|
||||||
|
dataArr.enumerated().forEach {
|
||||||
|
let text = $1
|
||||||
|
let normalStrig = NSMutableAttributedString(string: $1)
|
||||||
|
normalStrig.yy_color = ._999999
|
||||||
|
normalStrig.yy_font = .font(ofSize: 16, weight: .init(900))
|
||||||
|
|
||||||
|
let selectedString = NSMutableAttributedString(string: $1)
|
||||||
|
selectedString.yy_color = ._35_A_4_FE
|
||||||
|
selectedString.yy_font = .font(ofSize: 16, weight: .init(900))
|
||||||
|
|
||||||
|
|
||||||
|
var config = UIButton.Configuration.plain()
|
||||||
|
config.background.backgroundColor = .clear
|
||||||
|
config.contentInsets = .zero
|
||||||
|
|
||||||
|
let button = UIButton(configuration: config)
|
||||||
|
button.tag = $0
|
||||||
|
button.configurationUpdateHandler = { button in
|
||||||
|
let font = UIFont.font(ofSize: 16, weight: .init(900))
|
||||||
|
|
||||||
|
if button.isSelected {
|
||||||
|
button.configuration?.attributedTitle = AttributedString(text, attributes: AttributeContainer([
|
||||||
|
.font : font,
|
||||||
|
.foregroundColor : UIColor._35_A_4_FE
|
||||||
|
]))
|
||||||
|
} else {
|
||||||
|
button.configuration?.attributedTitle = AttributedString(text, attributes: AttributeContainer([
|
||||||
|
.font : font,
|
||||||
|
.foregroundColor : UIColor._999999
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addAction(UIAction(handler: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.selectedIndex = button.tag
|
||||||
|
self.didSelectedIndex?(self.selectedIndex)
|
||||||
|
}), for: .touchUpInside)
|
||||||
|
|
||||||
|
button.isSelected = $0 == selectedIndex
|
||||||
|
|
||||||
|
self.scrollView.addSubview(button)
|
||||||
|
self.buttonArr.append(button)
|
||||||
|
|
||||||
|
if previousButton == nil {
|
||||||
|
button.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(30)
|
||||||
|
make.top.equalToSuperview()
|
||||||
|
make.height.equalTo(35)
|
||||||
|
}
|
||||||
|
} else if let previousButton = previousButton, count - 1 == $0 {
|
||||||
|
button.snp.makeConstraints { make in
|
||||||
|
make.top.equalToSuperview()
|
||||||
|
make.left.equalTo(previousButton.snp.right).offset(40)
|
||||||
|
make.height.equalTo(35)
|
||||||
|
make.right.equalToSuperview().offset(-30)
|
||||||
|
}
|
||||||
|
} else if let previousButton = previousButton {
|
||||||
|
button.snp.makeConstraints { make in
|
||||||
|
make.top.equalToSuperview()
|
||||||
|
make.left.equalTo(previousButton.snp.right).offset(40)
|
||||||
|
make.height.equalTo(35)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if button.isSelected {
|
||||||
|
self.updateLinePosition(to: button, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousButton = button
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@objc private func handleButton(sender: UIButton) {
|
||||||
|
self.selectedIndex = sender.tag
|
||||||
|
self.didSelectedIndex?(self.selectedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLinePosition(to button: UIButton, _ isAnimate: Bool) {
|
||||||
|
|
||||||
|
lineView.snp.remakeConstraints { make in
|
||||||
|
make.bottom.equalTo(button)
|
||||||
|
make.width.equalTo(10)
|
||||||
|
make.height.equalTo(4)
|
||||||
|
make.centerX.equalTo(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAnimate {
|
||||||
|
UIView.animate(withDuration: 0.3) {
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAEpMenuView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(scrollView)
|
||||||
|
scrollView.addSubview(lineView)
|
||||||
|
|
||||||
|
scrollView.snp.makeConstraints { make in
|
||||||
|
make.left.right.top.equalToSuperview()
|
||||||
|
make.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
Fableon/Class/Player/V/FAEpSelectorCell.swift
Normal file
41
Fableon/Class/Player/V/FAEpSelectorCell.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// FAEpSelectorCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FAEpSelectorCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
var model: FAVideoInfoModel? {
|
||||||
|
didSet {
|
||||||
|
numberLabel.text = model?.episode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fa_isSelected: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if fa_isSelected {
|
||||||
|
numberLabel.textColor = ._35_A_4_FE
|
||||||
|
self.contentView.backgroundColor = .FFFFFF
|
||||||
|
self.contentView.layer.borderColor = UIColor._35_A_4_FE.cgColor
|
||||||
|
} else {
|
||||||
|
numberLabel.textColor = .FFFFFF.withAlphaComponent(0.8)
|
||||||
|
self.contentView.backgroundColor = .FFFFFF.withAlphaComponent(0.25)
|
||||||
|
self.contentView.layer.borderColor = UIColor.clear.cgColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBOutlet weak var numberLabel: UILabel!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
self.layer.masksToBounds = false
|
||||||
|
self.contentView.layer.cornerRadius = 3
|
||||||
|
self.contentView.layer.borderWidth = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
40
Fableon/Class/Player/V/FAEpSelectorCell.xib
Normal file
40
Fableon/Class/Player/V/FAEpSelectorCell.xib
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FAEpSelectorCell" customModule="Fableon" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="185" height="250"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="185" height="250"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yhy-MQ-hyT">
|
||||||
|
<rect key="frame" x="88.666666666666671" y="115.33333333333333" width="8" height="19.333333333333329"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="16"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="yhy-MQ-hyT" firstAttribute="centerX" secondItem="gTV-IL-0wX" secondAttribute="centerX" id="CFQ-ov-ult"/>
|
||||||
|
<constraint firstItem="yhy-MQ-hyT" firstAttribute="centerY" secondItem="gTV-IL-0wX" secondAttribute="centerY" id="Ov2-Ct-sBR"/>
|
||||||
|
</constraints>
|
||||||
|
<size key="customSize" width="185" height="250"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="numberLabel" destination="yhy-MQ-hyT" id="SSw-lq-oFo"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="241.98473282442748" y="111.9718309859155"/>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
259
Fableon/Class/Player/V/FAEpSelectorView.swift
Normal file
259
Fableon/Class/Player/V/FAEpSelectorView.swift
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
//
|
||||||
|
// FAEpSelectorView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import HWPanModal
|
||||||
|
|
||||||
|
class FAEpSelectorView: FAPanModalContentView {
|
||||||
|
|
||||||
|
var didSelected: ((_ index: Int) -> Void)?
|
||||||
|
|
||||||
|
var model: FAShortDetailModel? {
|
||||||
|
didSet {
|
||||||
|
titleLabel.text = model?.shortPlayInfo?.name
|
||||||
|
|
||||||
|
collectionView.reloadData()
|
||||||
|
|
||||||
|
let epList = model?.episodeList ?? []
|
||||||
|
|
||||||
|
var menuDataArr = [String]()
|
||||||
|
let totalEpisode = epList.count
|
||||||
|
var index = 0
|
||||||
|
var remainingEpisodes = totalEpisode
|
||||||
|
|
||||||
|
while remainingEpisodes > 0 {
|
||||||
|
let minIndex = index * 24
|
||||||
|
var maxIndex = minIndex + 23
|
||||||
|
if maxIndex >= epList.count {
|
||||||
|
maxIndex = epList.count - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let minEpisode = epList[minIndex].episode ?? "0"
|
||||||
|
let maxEpisode = epList[maxIndex].episode ?? "0"
|
||||||
|
|
||||||
|
if minEpisode == maxEpisode {
|
||||||
|
menuDataArr.append("\(minEpisode)")
|
||||||
|
} else {
|
||||||
|
menuDataArr.append("\(minEpisode)-\(maxEpisode)")
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingEpisodes -= 24
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
self.menuView.dataArr = menuDataArr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedIndex: Int = 0 {
|
||||||
|
didSet {
|
||||||
|
collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isDecelerating = false
|
||||||
|
private var isDragging = false
|
||||||
|
|
||||||
|
private lazy var titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 16, weight: .medium)
|
||||||
|
label.textColor = .FFFFFF
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var menuView: FAEpMenuView = {
|
||||||
|
let view = FAEpMenuView()
|
||||||
|
view.didSelectedIndex = { [weak self] index in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let epList = self.model?.episodeList ?? []
|
||||||
|
var row = 0
|
||||||
|
if index > 0 {
|
||||||
|
row = index * 24 + 10
|
||||||
|
let count = epList.count
|
||||||
|
if row >= count {
|
||||||
|
row = count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let indexPath = IndexPath.init(row: row, section: 0)
|
||||||
|
self.collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var cvLayout: FAWaterfallFlowLayout = {
|
||||||
|
let layout = FAWaterfallFlowLayout()
|
||||||
|
layout.delegate = self
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectionView: FACollectionView = {
|
||||||
|
let collectionView = FACollectionView(frame: .zero, collectionViewLayout: cvLayout)
|
||||||
|
collectionView.contentInset = .init(top: 10, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.register(UINib(nibName: "FAEpSelectorCell", bundle: nil), forCellWithReuseIdentifier: "cell")
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
self.contentHeight = 350 + UIScreen.safeBottom
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func backgroundConfig() -> HWBackgroundConfig {
|
||||||
|
let config = HWBackgroundConfig()
|
||||||
|
config.backgroundAlpha = 0.6
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func present(in view: UIView?) {
|
||||||
|
super.present(in: view)
|
||||||
|
self.hw_contentView.fa_addEffectView(style: .dark)
|
||||||
|
let r = self.cornerRadius()
|
||||||
|
self.hw_contentView.fa_setRoundedCorner(topLeft: r, topRight: r, bottomLeft: 0, bottomRight: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension FAEpSelectorView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(menuView)
|
||||||
|
addSubview(collectionView)
|
||||||
|
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(20)
|
||||||
|
make.right.lessThanOrEqualToSuperview().offset(-20)
|
||||||
|
make.top.equalToSuperview().offset(18)
|
||||||
|
}
|
||||||
|
|
||||||
|
menuView.snp.makeConstraints { make in
|
||||||
|
make.left.right.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(55)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionView.snp.makeConstraints { make in
|
||||||
|
make.left.right.bottom.equalToSuperview()
|
||||||
|
make.top.equalToSuperview().offset(107)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: UICollectionViewDelegate, UICollectionViewDataSource
|
||||||
|
extension FAEpSelectorView: UICollectionViewDelegate, UICollectionViewDataSource {
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! FAEpSelectorCell
|
||||||
|
cell.model = model?.episodeList?[indexPath.row]
|
||||||
|
cell.fa_isSelected = selectedIndex == indexPath.row
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
model?.episodeList?.count ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
if self.selectedIndex == indexPath.row { return }
|
||||||
|
self.selectedIndex = indexPath.row
|
||||||
|
|
||||||
|
self.didSelected?(indexPath.row)
|
||||||
|
self.dismiss(animated: true) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if isDragging || isDecelerating {
|
||||||
|
updateMuneSelectedIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
isDecelerating = false
|
||||||
|
updateMuneSelectedIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
isDecelerating = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
isDragging = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||||
|
isDragging = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMuneSelectedIndex() {
|
||||||
|
let epList = model?.episodeList ?? []
|
||||||
|
let indexPathArr = collectionView.indexPathsForVisibleItems
|
||||||
|
|
||||||
|
var minRow = epList.count - 1
|
||||||
|
var maxRow = 0
|
||||||
|
|
||||||
|
for indexPath in indexPathArr {
|
||||||
|
if indexPath.row < minRow {
|
||||||
|
minRow = indexPath.row
|
||||||
|
}
|
||||||
|
if indexPath.row > maxRow {
|
||||||
|
maxRow = indexPath.row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedIndex = maxRow / 24
|
||||||
|
if menuView.selectedIndex != selectedIndex {
|
||||||
|
menuView.selectedIndex = selectedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: FAWaterfallMutiSectionDelegate
|
||||||
|
extension FAEpSelectorView: FAWaterfallMutiSectionDelegate {
|
||||||
|
|
||||||
|
func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
|
||||||
|
return 44
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnNumber(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> Int {
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineSpacing(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||||
|
return 15
|
||||||
|
}
|
||||||
|
|
||||||
|
func interitemSpacing(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> CGFloat {
|
||||||
|
return 15
|
||||||
|
}
|
||||||
|
|
||||||
|
func insetForSection(collectionView collection: UICollectionView, layout: FAWaterfallFlowLayout, section: Int) -> UIEdgeInsets {
|
||||||
|
return .init(top: 0, left: 20, bottom: 0, right: 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
42
Fableon/Class/Player/V/FAPlayerDetailCell.swift
Normal file
42
Fableon/Class/Player/V/FAPlayerDetailCell.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// FAPlayerDetailCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/1.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import JXPlayer
|
||||||
|
|
||||||
|
class FAPlayerDetailCell: JXPlayerListCell {
|
||||||
|
|
||||||
|
override var ControlViewClass: JXPlayerListControlView.Type {
|
||||||
|
return FAPlayerDetailControlView.self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override var model: Any? {
|
||||||
|
didSet {
|
||||||
|
let model = self.model as? FAVideoInfoModel
|
||||||
|
self.player.setPlayUrl(url: model?.video_url ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortModel: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
let controlView = self.controlView as? FAPlayerDetailControlView
|
||||||
|
controlView?.shortModel = shortModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
235
Fableon/Class/Player/V/FAPlayerDetailControlView.swift
Normal file
235
Fableon/Class/Player/V/FAPlayerDetailControlView.swift
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
//
|
||||||
|
// FAPlayerDetailControlView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import JXPlayer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class FAPlayerDetailControlView: JXPlayerListControlView {
|
||||||
|
|
||||||
|
|
||||||
|
override var viewModel: JXPlayerListViewModel? {
|
||||||
|
didSet {
|
||||||
|
self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fa_viewModel: FAShortDetailViewModel? {
|
||||||
|
return self.viewModel as? FAShortDetailViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override var model: Any? {
|
||||||
|
didSet {
|
||||||
|
// let model = self.model as? FAVideoInfoModel
|
||||||
|
|
||||||
|
updateEp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortModel: FAShortPlayModel? {
|
||||||
|
didSet {
|
||||||
|
updateEp()
|
||||||
|
shortNameLabel.text = shortModel?.name
|
||||||
|
textLabel.text = shortModel?.fa_description
|
||||||
|
|
||||||
|
collectButton.isSelected = shortModel?.is_collect == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var durationTime: TimeInterval {
|
||||||
|
didSet {
|
||||||
|
updateProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var currentTime: TimeInterval {
|
||||||
|
didSet {
|
||||||
|
updateProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isCurrent: Bool {
|
||||||
|
didSet {
|
||||||
|
playButton.setNeedsUpdateConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var epButton: UIHostingController<FAPlayerEpUIButton> = {
|
||||||
|
let view = FAPlayerEpUIButton()
|
||||||
|
let hc = UIHostingController(rootView: view)
|
||||||
|
hc.view.backgroundColor = .clear
|
||||||
|
return hc
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var progressView: FAPlayerProgressView = {
|
||||||
|
let view = FAPlayerProgressView()
|
||||||
|
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 textLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 12, weight: .regular);
|
||||||
|
label.textColor = UIColor(named: .color_FFFFFF)!.withAlphaComponent(0.8)
|
||||||
|
label.numberOfLines = 2
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var shortNameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 16, weight: .medium)
|
||||||
|
label.textColor = UIColor(named: .color_FFFFFF)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var playButton: UIButton = {
|
||||||
|
let config = UIButton.Configuration.plain()
|
||||||
|
|
||||||
|
let button = UIButton(configuration: config, primaryAction: UIAction(handler: { [weak self] _ in
|
||||||
|
self?.fa_viewModel?.userSwitchPlayAndPause()
|
||||||
|
}))
|
||||||
|
button.configurationUpdateHandler = { [weak self] button in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if self.viewModel?.isPlaying == true || !isCurrent {
|
||||||
|
button.configuration?.image = UIImage(named: "pause_icon")
|
||||||
|
} else {
|
||||||
|
button.configuration?.image = UIImage(named: "play_icon_01")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectButton: UIButton = {
|
||||||
|
var config = UIButton.Configuration.plain()
|
||||||
|
config.background.backgroundColor = .clear
|
||||||
|
let button = UIButton(configuration: config)
|
||||||
|
button.configurationUpdateHandler = { [weak self] button in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if button.isSelected {
|
||||||
|
button.configuration?.image = UIImage(named: "collect_star_icon_selected")
|
||||||
|
} else {
|
||||||
|
button.configuration?.image = UIImage(named: "collect_star_icon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.addAction(UIAction(handler: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let shortPlayId = self.shortModel?.short_play_id else { return }
|
||||||
|
let videoId = (self.model as? FAVideoInfoModel)?.short_play_video_id
|
||||||
|
let isCollect = !(self.shortModel?.is_collect ?? false)
|
||||||
|
|
||||||
|
FAAPI.requestShortCollect(isCollect: isCollect, shortPlayId: shortPlayId, videoId: videoId, success: nil)
|
||||||
|
|
||||||
|
}), for: .touchUpInside)
|
||||||
|
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: FAAPI.updateShortCollectStateNotification, object: nil)
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "isPlaying" {
|
||||||
|
playButton.setNeedsUpdateConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateEp() {
|
||||||
|
let model = self.model as? FAVideoInfoModel
|
||||||
|
|
||||||
|
let text = "Ep.##".localizedReplace(text: model?.episode ?? "") + "/" + "Ep.##".localizedReplace(text: "\(shortModel?.episode_total ?? 0)")
|
||||||
|
var view = FAPlayerEpUIButton(text: text)
|
||||||
|
view.clickHandle = { [weak self] in
|
||||||
|
self?.fa_viewModel?.onEpSelectorView()
|
||||||
|
}
|
||||||
|
epButton.rootView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateProgress() {
|
||||||
|
guard durationTime > 0 else {
|
||||||
|
progressView.progress = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
progressView.progress = currentTime / durationTime
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func updateShortCollectStateNotification(sender: Notification) {
|
||||||
|
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 == self.shortModel?.short_play_id else { return }
|
||||||
|
self.shortModel?.is_collect = state
|
||||||
|
|
||||||
|
collectButton.isSelected = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAPlayerDetailControlView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
|
||||||
|
addSubview(epButton.view)
|
||||||
|
addSubview(progressView)
|
||||||
|
addSubview(textLabel)
|
||||||
|
addSubview(shortNameLabel)
|
||||||
|
addSubview(playButton)
|
||||||
|
addSubview(collectButton)
|
||||||
|
|
||||||
|
|
||||||
|
epButton.view.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.bottom.equalToSuperview().offset(-(UIScreen.safeBottom + 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview()
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.bottom.equalTo(epButton.view.snp.top).offset(-8)
|
||||||
|
}
|
||||||
|
|
||||||
|
textLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.right.lessThanOrEqualToSuperview().offset(-84)
|
||||||
|
make.bottom.equalTo(progressView.snp.top).offset(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
shortNameLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.bottom.equalTo(textLabel.snp.top).offset(-5)
|
||||||
|
make.right.lessThanOrEqualToSuperview().offset(-84)
|
||||||
|
}
|
||||||
|
|
||||||
|
playButton.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
collectButton.snp.makeConstraints { make in
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.safeTop)
|
||||||
|
make.right.equalToSuperview().offset(-16)
|
||||||
|
make.height.equalTo(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
211
Fableon/Class/Player/V/FAPlayerProgressView.swift
Normal file
211
Fableon/Class/Player/V/FAPlayerProgressView.swift
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
//
|
||||||
|
// FAPlayerProgressView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import YYText
|
||||||
|
|
||||||
|
class FAPlayerProgressView: 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(named: .color_FFFFFF)!.withAlphaComponent(0.2)
|
||||||
|
var currentProgress = UIColor(named: .color_FFFFFF)!
|
||||||
|
|
||||||
|
var lineWidth: CGFloat = 3
|
||||||
|
|
||||||
|
///加载中状态
|
||||||
|
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 insets: UIEdgeInsets = .init(top: 0, left: 16, bottom: 0, right: 16) {
|
||||||
|
didSet {
|
||||||
|
self.invalidateIntrinsicContentSize()
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) lazy var panGesture: UIPanGestureRecognizer = {
|
||||||
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
|
||||||
|
return pan
|
||||||
|
}()
|
||||||
|
|
||||||
|
private(set) lazy var tagGesture: UITapGestureRecognizer = {
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:)))
|
||||||
|
return tap
|
||||||
|
}()
|
||||||
|
|
||||||
|
///是否在滑动中
|
||||||
|
private var isPaning: Bool = false
|
||||||
|
|
||||||
|
private var gradientTimer: Timer?
|
||||||
|
|
||||||
|
private var gradientValue: CGFloat = 0
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
return .init(width: UIScreen.width, height: lineWidth + insets.top + insets.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
// self.backgroundColor = progressColor
|
||||||
|
self.backgroundColor = .clear
|
||||||
|
|
||||||
|
self.addGestureRecognizer(panGesture)
|
||||||
|
self.addGestureRecognizer(tagGesture)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleGradientTimer() {
|
||||||
|
gradientValue += 0.1
|
||||||
|
if gradientValue > 1 {
|
||||||
|
gradientValue = 0
|
||||||
|
}
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func draw(_ rect: CGRect) {
|
||||||
|
super.draw(rect)
|
||||||
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||||
|
let width = rect.width
|
||||||
|
|
||||||
|
let progressX = insets.left
|
||||||
|
let progressY = insets.top
|
||||||
|
let progressWidth = width - insets.left - insets.right
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///绘制进度
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension FAPlayerProgressView {
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
150
Fableon/Class/Player/VC/FAPlayerDetailViewController.swift
Normal file
150
Fableon/Class/Player/VC/FAPlayerDetailViewController.swift
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
//
|
||||||
|
// FAPlayerDetailViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/1.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import JXPlayer
|
||||||
|
import FDFullscreenPopGesture
|
||||||
|
|
||||||
|
|
||||||
|
class FAPlayerDetailViewController: JXPlayerListViewController {
|
||||||
|
|
||||||
|
var shortPlayId: String?
|
||||||
|
|
||||||
|
override var ViewModelClass: JXPlayerListViewModel.Type {
|
||||||
|
return FAShortDetailViewModel.self
|
||||||
|
}
|
||||||
|
|
||||||
|
var fa_viewModel: FAShortDetailViewModel {
|
||||||
|
return self.viewModel as! FAShortDetailViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var returnButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setImage(UIImage(named: "Frame 3011"), for: .normal)
|
||||||
|
button.addAction(UIAction(handler: { [weak self] _ in
|
||||||
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
}), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var epLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 18, weight: .regular)
|
||||||
|
label.textColor = .init(named: .color_FFFFFF)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.fd_interactivePopDisabled = true
|
||||||
|
view.backgroundColor = .init(named: .color_000000)
|
||||||
|
self.fa_viewModel.shortPlayId = shortPlayId ?? ""
|
||||||
|
self.register(FAPlayerDetailCell.self, forCellWithReuseIdentifier: "FAPlayerDetailCell")
|
||||||
|
self.delegate = self
|
||||||
|
self.dataSource = self
|
||||||
|
|
||||||
|
|
||||||
|
requestDetailList()
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
self.navigationController?.setNavigationBarHidden(true, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
self.viewModel.currentCell?.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var previousVideoUrl: String? {
|
||||||
|
return self.fa_viewModel.previousEpisode?.video_url
|
||||||
|
}
|
||||||
|
|
||||||
|
override var nextVideoUrl: String? {
|
||||||
|
return self.fa_viewModel.nextEpisode?.video_url
|
||||||
|
}
|
||||||
|
|
||||||
|
override func play() {
|
||||||
|
super.play()
|
||||||
|
|
||||||
|
let videoInfo = self.viewModel.currentCell?.model as? FAVideoInfoModel
|
||||||
|
FAAPI.requestCreatePlayHistory(videoId: videoInfo?.short_play_video_id, shortPlayId: videoInfo?.short_play_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAPlayerDetailViewController {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
view.addSubview(returnButton)
|
||||||
|
view.addSubview(epLabel)
|
||||||
|
|
||||||
|
returnButton.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.safeTop)
|
||||||
|
make.height.equalTo(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
epLabel.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalTo(returnButton)
|
||||||
|
make.left.equalTo(returnButton.snp.right).offset(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: JXPlayerListViewControllerDelegate JXPlayerListViewControllerDataSource
|
||||||
|
extension FAPlayerDetailViewController: JXPlayerListViewControllerDelegate, JXPlayerListViewControllerDataSource {
|
||||||
|
func jx_playerListViewController(_ viewController: JXPlayerListViewController, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = self.dequeueReusableCell(withReuseIdentifier: "FAPlayerDetailCell", for: indexPath) as! FAPlayerDetailCell
|
||||||
|
cell.model = self.fa_viewModel.dataArr[indexPath.section].episodeList?[indexPath.row]
|
||||||
|
cell.shortModel = self.fa_viewModel.dataArr[indexPath.section].shortPlayInfo
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func jx_playerListViewController(_ viewController: JXPlayerListViewController, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
self.fa_viewModel.dataArr[section].episodeList?.count ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func jx_numberOfSections(in viewController: JXPlayerListViewController) -> Int {
|
||||||
|
self.fa_viewModel.dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func jx_playerListViewController(_ viewController: JXPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) {
|
||||||
|
let model = self.fa_viewModel.dataArr[indexPath.section].episodeList?[indexPath.row]
|
||||||
|
epLabel.text = "Ep.\(model?.episode ?? "")"
|
||||||
|
}
|
||||||
|
|
||||||
|
func jx_shouldAutoScrollNextEpisode(_ viewController: JXPlayerListViewController) -> Bool {
|
||||||
|
if let _ = self.fa_viewModel.popView {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension FAPlayerDetailViewController {
|
||||||
|
|
||||||
|
private func requestDetailList() {
|
||||||
|
self.fa_viewModel.requestDetailData { [weak self] code in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
72
Fableon/Class/Player/VM/FAShortDetailViewModel.swift
Normal file
72
Fableon/Class/Player/VM/FAShortDetailViewModel.swift
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// Untitled.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/8/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import JXPlayer
|
||||||
|
|
||||||
|
//@MainActor
|
||||||
|
class FAShortDetailViewModel: JXPlayerListViewModel, ObservableObject {
|
||||||
|
|
||||||
|
private(set) var dataArr: [FAShortDetailModel] = []
|
||||||
|
|
||||||
|
var shortPlayId: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var previousEpisode: FAVideoInfoModel? {
|
||||||
|
guard dataArr.count > 0 else { return nil }
|
||||||
|
let detailModel = dataArr[self.currentIndexPath.section]
|
||||||
|
let row = self.currentIndexPath.row - 1
|
||||||
|
if row < 0 { return nil }
|
||||||
|
return detailModel.episodeList?[row]
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextEpisode: FAVideoInfoModel? {
|
||||||
|
guard dataArr.count > 0 else { return nil }
|
||||||
|
let detailModel = dataArr[self.currentIndexPath.section]
|
||||||
|
let row = self.currentIndexPath.row + 1
|
||||||
|
if row >= (detailModel.episodeList?.count ?? 0) { return nil }
|
||||||
|
|
||||||
|
return detailModel.episodeList?[row]
|
||||||
|
}
|
||||||
|
|
||||||
|
weak var popView: UIView?
|
||||||
|
|
||||||
|
|
||||||
|
func requestDetailData(completer: ((_ code: Int) -> Void)?) {
|
||||||
|
FAAPI.requestShortDetailData(shortPlayId: shortPlayId) { [weak self] model, code, msg in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let model = model {
|
||||||
|
self.dataArr.removeAll()
|
||||||
|
self.dataArr.append(model)
|
||||||
|
self.playerListVC?.reloadData {
|
||||||
|
self.playerListVC?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completer?(code ?? -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FAShortDetailViewModel {
|
||||||
|
|
||||||
|
func onEpSelectorView() {
|
||||||
|
let view = FAEpSelectorView()
|
||||||
|
view.selectedIndex = self.currentIndexPath.row
|
||||||
|
view.model = self.dataArr[currentIndexPath.section]
|
||||||
|
view.didSelected = { [weak self] index in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.playerListVC?.scrollToItem(indexPath: IndexPath(row: index, section: currentIndexPath.section), animated: false)
|
||||||
|
}
|
||||||
|
view.present(in: nil)
|
||||||
|
|
||||||
|
self.popView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
73
Fableon/Class/Recommend/C/FARecommendViewController.swift
Normal file
73
Fableon/Class/Recommend/C/FARecommendViewController.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// FARecommendViewController.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import JXPlayer
|
||||||
|
|
||||||
|
class FARecommendViewController: JXPlayerListViewController {
|
||||||
|
|
||||||
|
override var contentSize: CGSize {
|
||||||
|
return .init(width: UIScreen.width, height: UIScreen.height - UIScreen.tabBarHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var ViewModelClass: JXPlayerListViewModel.Type {
|
||||||
|
return FARecommendViewModel.self
|
||||||
|
}
|
||||||
|
|
||||||
|
var fa_viewModel: FARecommendViewModel {
|
||||||
|
return self.viewModel as! FARecommendViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
self.register(FARecommendPlayerCell.self, forCellWithReuseIdentifier: "cell")
|
||||||
|
|
||||||
|
self.delegate = self
|
||||||
|
self.dataSource = self
|
||||||
|
|
||||||
|
self.fa_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()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var previousVideoUrl: String? {
|
||||||
|
return self.fa_viewModel.previousEpisode?.video_url
|
||||||
|
}
|
||||||
|
|
||||||
|
override var nextVideoUrl: String? {
|
||||||
|
return self.fa_viewModel.nextEpisode?.video_url
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: JXPlayerListViewControllerDataSource
|
||||||
|
extension FARecommendViewController: JXPlayerListViewControllerDataSource {
|
||||||
|
func jx_playerListViewController(_ viewController: JXPlayerListViewController, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = self.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! FARecommendPlayerCell
|
||||||
|
cell.model = fa_viewModel.dataArr[indexPath.row]
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func jx_playerListViewController(_ viewController: JXPlayerListViewController, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
return fa_viewModel.dataArr.count
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: JXPlayerListViewControllerDelegate
|
||||||
|
extension FARecommendViewController: JXPlayerListViewControllerDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
35
Fableon/Class/Recommend/V/FARecommendPlayerCell.swift
Normal file
35
Fableon/Class/Recommend/V/FARecommendPlayerCell.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// FARecommendPlayerCell.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import JXPlayer
|
||||||
|
|
||||||
|
class FARecommendPlayerCell: JXPlayerListCell {
|
||||||
|
|
||||||
|
override var ControlViewClass: JXPlayerListControlView.Type {
|
||||||
|
return FARecommendPlayerControlView.self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override var model: Any? {
|
||||||
|
didSet {
|
||||||
|
let model = self.model as? FAShortPlayModel
|
||||||
|
let videoInfo = model?.video_info
|
||||||
|
|
||||||
|
self.player.setPlayUrl(url: videoInfo?.video_url ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
237
Fableon/Class/Recommend/V/FARecommendPlayerControlView.swift
Normal file
237
Fableon/Class/Recommend/V/FARecommendPlayerControlView.swift
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
//
|
||||||
|
// FARecommendPlayerControlView.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import JXPlayer
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class FARecommendPlayerControlView: JXPlayerListControlView {
|
||||||
|
|
||||||
|
override var viewModel: JXPlayerListViewModel? {
|
||||||
|
didSet {
|
||||||
|
self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fa_viewModel: FARecommendViewModel? {
|
||||||
|
return self.viewModel as? FARecommendViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override var model: Any? {
|
||||||
|
didSet {
|
||||||
|
let shortModel = self.model as? FAShortPlayModel
|
||||||
|
// let videoInfo = shortModel?.video_info
|
||||||
|
|
||||||
|
updateEp()
|
||||||
|
shortNameLabel.text = shortModel?.name
|
||||||
|
textLabel.text = shortModel?.fa_description
|
||||||
|
|
||||||
|
collectButton.isSelected = shortModel?.is_collect == true
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortModel: FAShortPlayModel? {
|
||||||
|
return self.model as? FAShortPlayModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override var durationTime: TimeInterval {
|
||||||
|
didSet {
|
||||||
|
updateProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var currentTime: TimeInterval {
|
||||||
|
didSet {
|
||||||
|
updateProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isCurrent: Bool {
|
||||||
|
didSet {
|
||||||
|
playButton.setNeedsUpdateConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var epButton: UIHostingController<FAPlayerEpUIButton> = {
|
||||||
|
let view = FAPlayerEpUIButton()
|
||||||
|
let hc = UIHostingController(rootView: view)
|
||||||
|
hc.view.backgroundColor = .clear
|
||||||
|
return hc
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var progressView: FAPlayerProgressView = {
|
||||||
|
let view = FAPlayerProgressView()
|
||||||
|
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 textLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 12, weight: .regular);
|
||||||
|
label.textColor = UIColor(named: .color_FFFFFF)!.withAlphaComponent(0.8)
|
||||||
|
label.numberOfLines = 2
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var shortNameLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .font(ofSize: 16, weight: .medium)
|
||||||
|
label.textColor = UIColor(named: .color_FFFFFF)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var playButton: UIButton = {
|
||||||
|
let config = UIButton.Configuration.plain()
|
||||||
|
|
||||||
|
let button = UIButton(configuration: config, primaryAction: UIAction(handler: { [weak self] _ in
|
||||||
|
self?.fa_viewModel?.userSwitchPlayAndPause()
|
||||||
|
}))
|
||||||
|
button.configurationUpdateHandler = { [weak self] button in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if self.viewModel?.isPlaying == true || !isCurrent {
|
||||||
|
button.configuration?.image = UIImage(named: "pause_icon")
|
||||||
|
} else {
|
||||||
|
button.configuration?.image = UIImage(named: "play_icon_01")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var collectButton: UIButton = {
|
||||||
|
var config = UIButton.Configuration.plain()
|
||||||
|
config.background.backgroundColor = .clear
|
||||||
|
let button = UIButton(configuration: config)
|
||||||
|
button.configurationUpdateHandler = { [weak self] button in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if button.isSelected {
|
||||||
|
button.configuration?.image = UIImage(named: "collect_star_icon_selected")
|
||||||
|
} else {
|
||||||
|
button.configuration?.image = UIImage(named: "collect_star_icon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.addAction(UIAction(handler: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let shortPlayId = self.shortModel?.short_play_id else { return }
|
||||||
|
let videoId = (self.model as? FAVideoInfoModel)?.short_play_video_id
|
||||||
|
let isCollect = !(self.shortModel?.is_collect ?? false)
|
||||||
|
|
||||||
|
FAAPI.requestShortCollect(isCollect: isCollect, shortPlayId: shortPlayId, videoId: videoId, success: nil)
|
||||||
|
|
||||||
|
}), for: .touchUpInside)
|
||||||
|
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: FAAPI.updateShortCollectStateNotification, object: nil)
|
||||||
|
|
||||||
|
fa_setupLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||||
|
if keyPath == "isPlaying" {
|
||||||
|
playButton.setNeedsUpdateConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateEp() {
|
||||||
|
// let model = self.model as? FAVideoInfoModel
|
||||||
|
let model = self.model as? FAShortPlayModel
|
||||||
|
let videoInfo = model?.video_info
|
||||||
|
|
||||||
|
|
||||||
|
let text = "Ep.##".localizedReplace(text: videoInfo?.episode ?? "") + "/" + "Ep.##".localizedReplace(text: "\(model?.episode_total ?? 0)")
|
||||||
|
var view = FAPlayerEpUIButton(text: text)
|
||||||
|
view.clickHandle = { [weak self] in
|
||||||
|
self?.fa_viewModel?.pushPlayerDetail(self?.shortModel)
|
||||||
|
}
|
||||||
|
epButton.rootView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateProgress() {
|
||||||
|
guard durationTime > 0 else {
|
||||||
|
progressView.progress = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
progressView.progress = currentTime / durationTime
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func updateShortCollectStateNotification(sender: Notification) {
|
||||||
|
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 == self.shortModel?.short_play_id else { return }
|
||||||
|
self.shortModel?.is_collect = state
|
||||||
|
|
||||||
|
collectButton.isSelected = state
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FARecommendPlayerControlView {
|
||||||
|
|
||||||
|
private func fa_setupLayout() {
|
||||||
|
|
||||||
|
addSubview(epButton.view)
|
||||||
|
addSubview(progressView)
|
||||||
|
addSubview(textLabel)
|
||||||
|
addSubview(shortNameLabel)
|
||||||
|
addSubview(playButton)
|
||||||
|
addSubview(collectButton)
|
||||||
|
|
||||||
|
|
||||||
|
epButton.view.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.bottom.equalToSuperview().offset(-10)
|
||||||
|
}
|
||||||
|
|
||||||
|
progressView.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview()
|
||||||
|
make.centerX.equalToSuperview()
|
||||||
|
make.bottom.equalTo(epButton.view.snp.top).offset(-8)
|
||||||
|
}
|
||||||
|
|
||||||
|
textLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.right.lessThanOrEqualToSuperview().offset(-84)
|
||||||
|
make.bottom.equalTo(progressView.snp.top).offset(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
shortNameLabel.snp.makeConstraints { make in
|
||||||
|
make.left.equalToSuperview().offset(16)
|
||||||
|
make.bottom.equalTo(textLabel.snp.top).offset(-5)
|
||||||
|
make.right.lessThanOrEqualToSuperview().offset(-84)
|
||||||
|
}
|
||||||
|
|
||||||
|
playButton.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
collectButton.snp.makeConstraints { make in
|
||||||
|
make.top.equalToSuperview().offset(UIScreen.safeTop)
|
||||||
|
make.right.equalToSuperview().offset(-16)
|
||||||
|
make.height.equalTo(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
84
Fableon/Class/Recommend/VM/FARecommendViewModel.swift
Normal file
84
Fableon/Class/Recommend/VM/FARecommendViewModel.swift
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// FARecommendViewModel.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import JXPlayer
|
||||||
|
|
||||||
|
class FARecommendViewModel: JXPlayerListViewModel {
|
||||||
|
|
||||||
|
private(set) var dataArr: [FAShortPlayModel] = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var previousEpisode: FAVideoInfoModel? {
|
||||||
|
guard dataArr.count > 0 else { return nil }
|
||||||
|
let row = self.currentIndexPath.row - 1
|
||||||
|
if row < 0 { return nil }
|
||||||
|
return dataArr[row].video_info
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextEpisode: FAVideoInfoModel? {
|
||||||
|
guard dataArr.count > 0 else { return nil }
|
||||||
|
let row = self.currentIndexPath.row + 1
|
||||||
|
if row >= dataArr.count { return nil }
|
||||||
|
return dataArr[row].video_info
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addDataArr(dataArr: [FAShortPlayModel]) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushPlayerDetail(_ model: FAShortPlayModel?) {
|
||||||
|
guard let model = model else { return }
|
||||||
|
|
||||||
|
let vc = FAPlayerDetailViewController()
|
||||||
|
vc.shortPlayId = model.short_play_id
|
||||||
|
FATool.topViewController?.navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension FARecommendViewModel {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func requestDataArr(page: Int, completer: (() -> Void)? = nil) {
|
||||||
|
|
||||||
|
FAAPI.requestRecommendVideo(page: page) { [weak self] listModel in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let listModel = listModel, let list = listModel.list {
|
||||||
|
if page == 1 {
|
||||||
|
self.playerListVC?.clearData()
|
||||||
|
self.dataArr = list
|
||||||
|
self.playerListVC?.reloadData { [weak self] in
|
||||||
|
|
||||||
|
self?.playerListVC?.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.addDataArr(dataArr: list)
|
||||||
|
}
|
||||||
|
// self.pagination = listModel.pagination
|
||||||
|
}
|
||||||
|
completer?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
Fableon/Libs/Empty/FAEmpty.swift
Normal file
27
Fableon/Libs/Empty/FAEmpty.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// FAEmpty.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/10/14.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import LYEmptyView
|
||||||
|
|
||||||
|
|
||||||
|
struct FAEmpty {
|
||||||
|
|
||||||
|
static func fa_emptyView(image: UIImage?, title: String?) -> LYEmptyView {
|
||||||
|
|
||||||
|
let view = LYEmptyView.emptyActionView(with: image, titleStr: title, detailStr: nil, btnTitleStr: nil) {
|
||||||
|
// btnClickBlock?()
|
||||||
|
}
|
||||||
|
|
||||||
|
view?.titleLabFont = .font(ofSize: 14, weight: .medium)
|
||||||
|
view?.titleLabTextColor = .FFFFFF
|
||||||
|
view?.contentViewOffset = -100
|
||||||
|
view?.subViewMargin = 25
|
||||||
|
return view!
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Fableon/Libs/FADeviceId/FADeviceIDManager.swift
Normal file
26
Fableon/Libs/FADeviceId/FADeviceIDManager.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// FADeviceIDManager.swift
|
||||||
|
// Fableon
|
||||||
|
//
|
||||||
|
// Created by 长沙鸿瑶 on 2025/9/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FADeviceIDManager {
|
||||||
|
static let shared = FADeviceIDManager()
|
||||||
|
private let key = "com.fableon.uniqueDeviceID"
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
lazy var id: String = {
|
||||||
|
if let savedID = FAKeychainHelper.shared.read(key: key) {
|
||||||
|
return savedID
|
||||||
|
} else {
|
||||||
|
let newID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||||||
|
FAKeychainHelper.shared.save(key: key, value: newID)
|
||||||
|
return newID
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
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