首次提交

This commit is contained in:
湖北秦九 2025-11-20 16:59:32 +08:00
parent 78f4a215cc
commit b89399a769
247 changed files with 9791 additions and 1 deletions

4
.gitignore vendored
View File

@ -14,6 +14,7 @@ xcuserdata/
*.dSYM.zip *.dSYM.zip
*.dSYM *.dSYM
## Playgrounds ## Playgrounds
timeline.xctimeline timeline.xctimeline
playground.xcworkspace playground.xcworkspace
@ -38,7 +39,8 @@ 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/
Podfile.lock
# #
# 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

35
Podfile Normal file
View File

@ -0,0 +1,35 @@
# Uncomment the next line to define a global platform for your project
platform :ios, '13.0'
source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
config.build_settings['EXCLUDED_ARCHITECTURES'] = 'i386'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end
end
target 'SynthReel' do
use_frameworks!
pod 'YYCategories'
pod 'YYText'
pod 'Kingfisher'
pod 'SmartCodable'
pod 'Moya'
pod 'SVProgressHUD'
pod 'Toast'
pod 'JXSegmentedView'
pod 'JXPagingView/Paging'
pod 'FSPagerView'
pod 'JXPlayer', '~> 0.1.8'
pod 'MJRefresh'
pod 'collection-view-layouts/TagsLayout'
pod 'HWPanModal'
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

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

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:SynthReel.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,24 @@
{
"originHash" : "0e62262c59a183f44748a161870cc0f2b76e1b0e46f648559704e4be9de523b9",
"pins" : [
{
"identity" : "estabbarcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eggswift/ESTabBarController.git",
"state" : {
"revision" : "93a30b833a05fd916c6d4c5d6e94a270cf3b6636",
"version" : "2.9.0"
}
},
{
"identity" : "snapkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SnapKit/SnapKit",
"state" : {
"revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4",
"version" : "5.7.1"
}
}
],
"version" : 3
}

View File

@ -0,0 +1,92 @@
//
// SRHomeApi.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import Alamofire
struct SRHomeApi {
static func requestCategoryList() async -> [SRCategoryModel]? {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/getCategories")
param.method = .get
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRNetwork.List<SRCategoryModel>>) in
continuation.resume(returning: response.data?.list)
}
}
}
static func requestHomeModulesData() async -> [SRHomeModuleItem]? {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/home/all-modules")
param.method = .get
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRNetwork.List<SRHomeModuleItem>>) in
continuation.resume(returning: response.data?.list)
}
}
}
static func requestCategoryVideoData(_ id: String, page: Int) async -> [SRShortModel]? {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/videoList")
param.method = .get
param.parameters = [
"category_id" : id,
"current_page" : page,
"page_size" : 20
]
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRNetwork.List<SRShortModel>>) in
continuation.resume(returning: response.data?.list)
}
}
}
static func requestHotSearchData() async -> [SRShortModel]? {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/search/hots")
param.method = .get
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRNetwork.List<SRShortModel>>) in
continuation.resume(returning: response.data?.list)
}
}
}
static func requestSearch(_ text: String) async -> [SRShortModel]? {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/search")
param.method = .get
param.parameters = [
"search" : text
]
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRNetwork.List<SRShortModel>>) in
continuation.resume(returning: response.data?.list)
}
}
}
static func requestHomeRecommendData(page: Int) async -> [SRShortModel]? {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/getRecommands")
param.method = .get
param.parameters = [
"page_size" : 20,
"current_page" : page
]
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRNetwork.List<SRShortModel>>) in
continuation.resume(returning: response.data?.list)
}
}
}
}

View File

@ -0,0 +1,88 @@
//
// SRShortApi.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import Alamofire
struct SRShortApi {
static func requestShortDetail(_ id: String) async -> (SRShortDetailModel?, Int?, String?) {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/getVideoDetails")
param.method = .get
param.parameters = [
"short_play_id" : id,
"video_id" : 0,
]
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRShortDetailModel>) in
if response.isSuccess {
continuation.resume(returning:(response.data, response.code, response.msg))
} else {
continuation.resume(returning:(nil, response.code, response.msg))
}
}
}
}
static func requestShortCollect(shortId: String, videoId: String?, isCollect: Bool) async -> Bool {
await withCheckedContinuation { continuation in
var path = ""
if isCollect {
path = "/collect"
} else {
path = "/cancelCollect"
}
var param = SRNetwork.Parameters(path: path)
param.isLoding = true
param.parameters = [
"short_play_id" : shortId,
"video_id" : videoId ?? "0",
]
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRShortDetailModel>) in
if response.isSuccess {
continuation.resume(returning: true)
NotificationCenter.default.post(name: SRShortApi.updateShortCollectStateNotification, object: nil, userInfo: [
"state" : isCollect,
"id" : shortId,
])
} else {
continuation.resume(returning: false)
}
}
}
}
static func requestCreatePlayHistory(shortId: String?, videoId: String?) async {
guard let shortId = shortId else { return }
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/createHistory")
param.isToast = false
param.parameters = [
"short_play_id" : shortId,
"video_id" : videoId ?? "0",
]
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRShortDetailModel>) in
continuation.resume()
}
}
}
}
extension SRShortApi {
/// [ "state" : isCollect, "id" : shortPlayId,]
static let updateShortCollectStateNotification = NSNotification.Name(rawValue: "SRShortApi.updateShortCollectStateNotification")
}

View File

@ -0,0 +1,25 @@
//
// SRUserApi.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import Foundation
import Alamofire
struct SRUserApi {
static func requestUserInfo() async -> SRUserInfo? {
await withCheckedContinuation { continuation in
var param = SRNetwork.Parameters(path: "/customer/info")
param.method = .get
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRUserInfo>) in
continuation.resume(returning: response.data)
}
}
}
}

View File

@ -0,0 +1,24 @@
//
// SRDefine.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
///app
let kSRAPPVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0"
let kSRAPPBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0"
let kSRAPPBundleName: String = (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? ""
let kSRAPPName: String = (Bundle.main.infoDictionary!["CFBundleDisplayName"] as? String) ?? ""
#if DEBUG
public func srPrint(message: Any? , file: String = #file, function: String = #function, line: Int = #line) {
print("\n\(Date(timeIntervalSinceNow: 8 * 60 * 60)) \(file.components(separatedBy: "/").last ?? "") \(function) \(line): \(message ?? "")")
}
#else
public func srPrint(message: Any?) { }
#endif

View File

@ -0,0 +1,14 @@
//
// SRUserDefaultsKey.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
///token
let kSRAccountTokenDefaultsKey = "kSRAccountTokenDefaultsKey"
///
let kSRUserInfoDefaultsKey = "kSRUserInfoDefaultsKey"

View File

@ -0,0 +1,43 @@
//
// NSNumber+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
extension NSNumber {
func toString(maximumFractionDigits: Int = 10, minimumFractionDigits: Int? = nil, roundingMode: NumberFormatter.RoundingMode? = nil) -> String {
let formatter = NumberFormatter()
formatter.minimumIntegerDigits = 1
formatter.maximumFractionDigits = maximumFractionDigits
if let minimumFractionDigits = minimumFractionDigits {
formatter.minimumFractionDigits = minimumFractionDigits
}
if let roundingMode = roundingMode {
formatter.roundingMode = roundingMode
}
formatter.numberStyle = .none
return formatter.string(from: self) ?? "0"
}
}
extension Int {
func formatTimeGroup() -> (String, String, String) {
let seconds = self
var s: String = "00"
var m: String = "00"
var h: String = "00"
s = String(format: "%02d", Int(Int(seconds) % 60))
m = String(format: "%02d", Int(seconds / 60) % 60)
h = String(format: "%02d", Int(seconds / 3600))
return (h, m, s)
}
}

View File

@ -0,0 +1,26 @@
//
// String+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import YYCategories
extension String {
var localized: String {
return String(localized: LocalizationValue(self))
}
func localizedReplace(text: String) -> String {
return self.localized.replacingOccurrences(of: "##", with: text)
}
func size(_ font: UIFont, _ size: CGSize) -> CGSize {
return (self as NSString).size(for: font, size: size, mode: .byWordWrapping)
}
}

View File

@ -0,0 +1,18 @@
//
// UIFont+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
extension UIFont {
static func font(ofSize: CGFloat, weight: Weight) -> UIFont {
return .systemFont(ofSize: ofSize, weight: weight)
}
}

View File

@ -0,0 +1,50 @@
//
// UIScreen+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
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 SRTool.keyWindow?.safeAreaInsets.top ?? 20
}
static var safeBottom: CGFloat {
return SRTool.keyWindow?.safeAreaInsets.bottom ?? 0
}
static var navBarHeight: CGFloat {
return safeTop + 44
}
static var tabBarHeight: CGFloat {
return safeBottom + 49
}
///
static var widthRatio: CGFloat {
return UIScreen.width / 375
}
static func getRatioWidth(size: CGFloat) -> CGFloat {
return self.widthRatio * size
}
}

View File

@ -0,0 +1,46 @@
//
// UIScrollView+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import MJRefresh
extension UIScrollView {
func sr_addRefreshHeader(insetTop: CGFloat = 0, block: (() -> Void)?) {
self.mj_header = MJRefreshNormalHeader(refreshingBlock: {
block?()
})
self.mj_header?.ignoredScrollViewContentInsetTop = insetTop
}
func sr_addRefreshFooter(insetBottom: CGFloat = 0, block: (() -> Void)?) {
self.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: {
block?()
})
self.mj_footer?.ignoredScrollViewContentInsetBottom = insetBottom
}
func sr_endHeaderRefreshing() {
self.mj_header?.endRefreshing()
}
func sr_endFooterRefreshing() {
if self.mj_footer?.state == .noMoreData { return }
self.mj_footer?.endRefreshing()
}
///
func sr_resetNoMoreData() {
self.mj_footer?.resetNoMoreData()
}
func sr_endRefreshingWithNoMoreData() {
self.mj_footer?.endRefreshingWithNoMoreData()
}
}

View File

@ -0,0 +1,22 @@
//
// UIStackView+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
extension UIStackView {
func sr_removeAllArrangedSubview() {
let arrangedSubviews = self.arrangedSubviews
arrangedSubviews.forEach {
self.removeArrangedSubview($0)
$0.removeFromSuperview()
}
}
}

View File

@ -0,0 +1,92 @@
//
// UIView+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
extension UIView {
///
func applyHexagonMask(_ cornerRadius: CGFloat = 0) {
let width = bounds.width
let height = bounds.height
let center = CGPoint(x: width / 2, y: height / 2)
let radius = max(width, height) / 2
let path = UIBezierPath()
// 6
var points: [CGPoint] = []
for i in 0..<6 {
let angle = CGFloat(i) * (.pi / 3)
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
points.append(CGPoint(x: x, y: y))
}
//
for i in 0..<6 {
let prev = points[(i + 5) % 6] //
let curr = points[i] //
let next = points[(i + 1) % 6] //
//
let v1 = CGPoint(x: curr.x - prev.x, y: curr.y - prev.y)
let v2 = CGPoint(x: curr.x - next.x, y: curr.y - next.y)
let l1 = sqrt(v1.x * v1.x + v1.y * v1.y)
let l2 = sqrt(v2.x * v2.x + v2.y * v2.y)
let u1 = CGPoint(x: v1.x / l1, y: v1.y / l1)
let u2 = CGPoint(x: v2.x / l2, y: v2.y / l2)
// cornerRadius
let p1 = CGPoint(x: curr.x - u1.x * cornerRadius,
y: curr.y - u1.y * cornerRadius)
let p2 = CGPoint(x: curr.x - u2.x * cornerRadius,
y: curr.y - u2.y * cornerRadius)
if i == 0 {
path.move(to: p1)
} else {
path.addLine(to: p1)
}
// 线
path.addQuadCurve(to: p2, controlPoint: curr)
}
path.close()
// mask
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
layer.mask = maskLayer
}
func addHollowHole(holePath: UIBezierPath, dimColor: UIColor = UIColor.black.withAlphaComponent(0.6)) {
// 1.
let fullPath = UIBezierPath(rect: self.bounds)
// 2.
fullPath.append(holePath)
fullPath.usesEvenOddFillRule = true
// 3. layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = fullPath.cgPath
shapeLayer.fillRule = .evenOdd
shapeLayer.fillColor = dimColor.cgColor //
shapeLayer.opacity = 1.0
// self.layer.sublayers?.forEach { $0.removeFromSuperlayer() } //
// self.layer.addSublayer(shapeLayer)
self.layer.mask = shapeLayer
}
}

View File

@ -0,0 +1,46 @@
//
// UserDefaults+SRAdd.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import Foundation
extension UserDefaults {
static func sr_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 sr_object<T: NSObject & NSSecureCoding>(forKey key: String, as type: T.Type) -> T? {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: key) else {
return nil
}
do {
let object = try NSKeyedUnarchiver.unarchivedObject(ofClass: type, from: data)
return object
} catch {
print("Error unarchiving object: \(error)")
return nil
}
}
}

View File

@ -0,0 +1,193 @@
//
// SRNetwork.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import Moya
import SmartCodable
class SRNetwork: NSObject {
private static let operationQueue = OperationQueue()
private static var tokenOperation: BlockOperation?
static let provider = MoyaProvider<SRTargetType>()
static func request<T>(parameters: SRNetwork.Parameters, completion: ((_ response: SRNetwork.Response<T>) -> Void)?) {
if SRAccountManager.manager.token == nil {
self.requestToken(completer: nil)
}
if let tokenOperation = self.tokenOperation, parameters.path != "/customer/register" {
let requestOperation = BlockOperation {
let semaphore = DispatchSemaphore(value: 0)
_request(parameters: parameters) { (response: SRNetwork.Response<T>) in
semaphore.signal()
completion?(response)
}
semaphore.wait()
}
///
requestOperation.addDependency(tokenOperation)
operationQueue.addOperation(requestOperation)
} else {
_request(parameters: parameters, completion: completion)
}
}
@discardableResult
static func _request<T>(parameters: SRNetwork.Parameters, completion: ((_ response: SRNetwork.Response<T>) -> Void)?) -> Cancellable {
if parameters.isLoding {
SRHud.show()
}
return provider.request(.request(parameters: parameters)) { result in
if parameters.isLoding {
SRHud.dismiss()
}
guard let completion = completion else {return}
_resultDispose(parameters: parameters, result: result, completion: completion)
}
}
private static func _resultDispose<T>(parameters: SRNetwork.Parameters, result: Result<Moya.Response, MoyaError>, completion: ((_ response: SRNetwork.Response<T>) -> Void)?) {
switch result {
case .success(let response):
let code = response.statusCode
if code == 401 || code == 402 || code == 403 {
if parameters.path == "/customer/register" {
var res = SRNetwork.Response<T>()
res.code = -1
if parameters.isToast {
// BRToast.show(text: "Error".localized)
}
completion?(res)
} else {
// if code == 402, parameters.isToast {
// BRToast.show(text: "beereel_network_error_1".localized)
// }
///token
self.requestToken { token in
if token != nil {
_Concurrency.Task {
await SRAccountManager.manager.updateUserInfo()
}
}
}
///
if let tokenOperation = self.tokenOperation, parameters.path != "/customer/register" {
let requestOperation = BlockOperation {
let semaphore = DispatchSemaphore(value: 0)
_request(parameters: parameters) { (response: SRNetwork.Response<T>) in
semaphore.signal()
completion?(response)
}
semaphore.wait()
}
///
requestOperation.addDependency(tokenOperation)
operationQueue.addOperation(requestOperation)
}
}
return
}
do {
let tempData = try response.mapString()
srPrint(message: parameters.parameters)
srPrint(message: parameters.path)
let response: SRNetwork.Response<T> = _deserialize(data: tempData)
if !response.isSuccess{
if parameters.isToast {
// BRToast.show(text: response.msg)
}
}
completion?(response)
} catch {
var res = SRNetwork.Response<T>()
res.code = -1
if parameters.isToast {
// BRToast.show(text: "Error".localized)
}
completion?(res)
}
case .failure(let error):
srPrint(message: error)
var res = SRNetwork.Response<T>()
res.code = -1
if parameters.isToast {
// BRToast.show(text: "beereel_network".localized)
}
completion?(res)
break
}
}
///
static private func _deserialize<T>(data: String) -> SRNetwork.Response<T> {
var response: SRNetwork.Response<T>?
let decrypted = SRResponseCryptor.decrypt(data: data)
srPrint(message: decrypted)
response = SRNetwork.Response<T>.deserialize(from: decrypted)
response?.rawData = decrypted
if let response = response {
return response
} else {
var response = SRNetwork.Response<T>()
response.code = -1
response.msg = "Error".localized
return response
}
}
}
extension SRNetwork {
///token
static func requestToken(completer: ((_ token: SRAccountToken?) -> Void)?) {
guard self.tokenOperation == nil else {
completer?(nil)
return
}
self.tokenOperation = BlockOperation(block: {
let semaphore = DispatchSemaphore(value: 0)
let param = SRNetwork.Parameters(path: "/customer/register")
DispatchQueue.main.async {
SRNetwork.request(parameters: param) { (response: SRNetwork.Response<SRAccountToken>) in
if let token = response.data {
SRAccountManager.manager.setAccountToken(token)
}
do { semaphore.signal() }
self.tokenOperation = nil
completer?(response.data)
}
}
semaphore.wait()
})
operationQueue.addOperation(self.tokenOperation!)
}
}

View File

@ -0,0 +1,62 @@
//
// SRNetworkModel.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SmartCodable
import Moya
import Alamofire
extension SRNetwork {
struct Parameters {
var baseURL: URL?
var parameters: [String : Any]?
var method: Moya.Method = .post
var path: String
var isLoding: Bool = false
var isToast: Bool = true
}
struct Response<T : SmartCodable>: SmartCodable {
var code: Int?
var data: T?
var msg: String?
@SmartIgnored
var rawData: Any?
var isSuccess: Bool {
return code == 200
}
}
struct List<T: SmartCodable>: SmartCodable {
var list: [T]?
var pagination: Pagination?
}
struct Pagination: SmartCodable {
var current_page: Int?
var page_size: Int?
var page_total: Int?
var total_size: Int?
}
}
//extension String: SmartCodable {
//
//}
//
//extension Int: SmartCodable {
//
//}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
//
// SRUrlPath.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
let SRBaseURL = "https://api-hbqinjiu.hbqinjiu.com"
let SRUrlPathPrefix = "/eon"
let SRWebBaseURL = "https://www.hbqinjiu.com"
let SRCampaignWebURL = "https://campaign.hbqinjiu.com"
/*
SynthReel
admin-api https://admin-api-synthreeltv.synthreeltv.com
api https://api-synthreeltv.synthreeltv.com/th
https://www.synthreeltv.com
https://www.synthreeltv.com/xxxxx
https://campaign.synthreeltv.com/pages/leave/index {theme:theme_16}
https://campaign.synthreeltv.com/pages/setting/logout {theme: theme_11, device-id:xxxx}
w2ahttps://w2a.synthreeltv.com/
Apihttps://api-synthreeltv.synthreeltv.com/th
*/

View File

@ -0,0 +1,24 @@
//
// SRCollectionView.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRCollectionView: UICollectionView {
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
self.backgroundColor = .clear
self.contentInsetAdjustmentBehavior = .never
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,45 @@
//
// SRGradientView.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRGradientView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
var locations: [NSNumber]? {
didSet {
self.gradientLayer.locations = locations
}
}
var colors: [CGColor]? {
didSet {
self.gradientLayer.colors = colors
}
}
var startPoint: CGPoint = .zero {
didSet {
self.gradientLayer.startPoint = startPoint
}
}
var endPoint: CGPoint = .zero {
didSet {
self.gradientLayer.endPoint = endPoint
}
}
}

View File

@ -0,0 +1,100 @@
//
// SRImageView.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import Kingfisher
class SRImageView: UIImageView {
var placeholderColor = UIColor.gray
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 sr_setImage(_ url: String?, placeholder: UIImage? = nil, completer: ((_ image: UIImage?, _ url: URL?) -> Void)? = nil) {
self.kf.setImage(with: URL(string: url ?? ""), placeholder: placeholder, options: nil) { result in
switch result {
case .success(let value):
completer?(value.image, value.source.url)
default :
completer?(nil, nil)
break
}
}
}
}

View File

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

View File

@ -0,0 +1,84 @@
//
// SRPanModalContentView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import HWPanModal
import SnapKit
class SRPanModalContentView: HWPanModalContentView {
var contentHeight = UIScreen.height * (2 / 3)
///UI contentSize
func setNeedsLayoutUpdate() {
self.panModalSetNeedsLayoutUpdate()
}
lazy var bgImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "pan_bg_image_01"))
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(bgImageView)
bgImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: HWPanModalPresentable
override func panScrollable() -> UIScrollView? {
return nil
}
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 0
}
}

View File

@ -0,0 +1,21 @@
//
// SRScrollView.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRScrollView: UIScrollView {
override init(frame: CGRect) {
super.init(frame: frame)
self.contentInsetAdjustmentBehavior = .never
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}

View File

@ -0,0 +1,49 @@
//
// SRTableView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRTableView: UITableView {
var insetGroupedMargins: CGFloat = 15
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
separatorInset = .init(top: 0, left: 15, bottom: 0, right: 15)
self.backgroundColor = .clear
self.contentInsetAdjustmentBehavior = .never
if style == .insetGrouped || style == .grouped {
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
}
}
}

View File

@ -0,0 +1,48 @@
//
// SRTableViewCell.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRTableViewCell: 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
}
}

View File

@ -0,0 +1,36 @@
//
// SRNavigationController.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
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)
}
}

View File

@ -0,0 +1,51 @@
//
// SRTabBarController.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import ESTabBarController
class SRTabBarController: ESTabBarController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ._010101
let nav1 = createNavigationView(SRHomeViewController(), image: UIImage(named: "tabbar_icon_01"), selectedImage: UIImage(named: "tabbar_icon_01_selected"))
let nav2 = createNavigationView(SRRecommendPlayerViewController(), image: UIImage(named: "tabbar_icon_02"), selectedImage: UIImage(named: "tabbar_icon_02_selected"))
let nav3 = createNavigationView(SRMyShortViewController(), image: UIImage(named: "tabbar_icon_03"), selectedImage: UIImage(named: "tabbar_icon_03_selected"))
let nav4 = createNavigationView(SRViewController(), image: UIImage(named: "tabbar_icon_04"), selectedImage: UIImage(named: "tabbar_icon_04_selected"))
viewControllers = [nav1, nav2, nav3, nav4]
let appearance = UITabBarAppearance()
appearance.backgroundColor = ._010101
appearance.shadowColor = .clear
appearance.backgroundImage = UIImage()
appearance.shadowImage = UIImage()
self.tabBar.standardAppearance = appearance
self.tabBar.scrollEdgeAppearance = appearance
self.tabBar.isTranslucent = false
}
func createNavigationView(_ viewController: UIViewController, image: UIImage?, selectedImage: UIImage?) -> UINavigationController {
let contentView = ESTabBarItemContentView()
contentView.itemContentMode = .alwaysOriginal
contentView.renderingMode = .alwaysOriginal
let tabBarItem = ESTabBarItem(contentView, image: image, selectedImage: selectedImage)
let nav = SRNavigationController(rootViewController: viewController)
nav.tabBarItem = tabBarItem
return nav
}
}

View File

@ -0,0 +1,82 @@
//
// SRViewController.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
import JXPagingView
class SRViewController: UIViewController {
var didScrollCallback: ((_ : UIScrollView) -> Void)?
lazy var backgroundImageView = UIImageView(image: UIImage(named: "background_image_01"))
override func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = [.top]
self.view.backgroundColor = ._010101
view.addSubview(backgroundImageView)
backgroundImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
}
}
func handleHeaderRefresh(_ completer: (() -> Void)?) {
completer?()
}
func handleFooterRefresh(_ completer: (() -> Void)?) {
completer?()
}
}
//MARK: JXPagingSmoothViewListViewDelegate
extension SRViewController: JXPagingSmoothViewListViewDelegate, JXPagingViewListViewDelegate {
func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> ()) {
self.didScrollCallback = callback
}
func listView() -> UIView {
return self.view
}
func listScrollView() -> UIScrollView {
return UIScrollView()
}
}
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 sr_handleNavigationBack() {
self.sr_toLastViewController(animated: true)
}
func sr_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)
}
}
}

View File

@ -0,0 +1,16 @@
//
// SRCategoryModel.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SmartCodable
struct SRCategoryModel: SmartCodable {
var id: String?
var name: String?
}

View File

@ -0,0 +1,56 @@
//
// SRHomeModuleItem.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SmartCodable
class SRHomeModuleItem: NSObject, SmartCodable {
required override init() { }
enum ModuleKey: String, SmartCaseDefaultable {
case banner = "home_banner"
///
case detailsRecommand = "get_details_recommand"
case popular = "home_v3_recommand"
case updates = "week_ranking"
case bingeWorthy = "week_highest_recommend"
case viralHits = "highest_payment_hot_video"
case premiereNow = "new_recommand"
}
var title: String?
var module_key: ModuleKey?
var list: [SRShortModel] = []
@SmartAny
var data: Any?
func didFinishMapping() {
if let data = data as? [[String : Any]] {
self.list = [SRShortModel].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 = [SRShortModel].deserialize(from: dataList) ?? []
}
}
}
}

View File

@ -0,0 +1,42 @@
//
// SRHomeBannerCell.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import FSPagerView
import SnapKit
class SRHomeBannerCell: FSPagerViewCell {
var model: SRShortModel? {
didSet {
if let image = model?.horizontally_img, image.count > 0 {
coverImageView.sr_setImage(image)
} else {
coverImageView.sr_setImage(model?.image_url)
}
}
}
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(coverImageView)
coverImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@MainActor required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,40 @@
//
// SRHomeBannerMiniCell.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeBannerMiniCell: UICollectionViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
}
}
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 2
imageView.layer.masksToBounds = true
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(coverImageView)
coverImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,177 @@
//
// SRHomeBannerView.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
import FSPagerView
class SRHomeBannerView: UIView {
var dataArr: [SRShortModel]? {
didSet {
self.bannerView.reloadData()
self.miniCollectionView.reloadData()
}
}
var didSelectedShort: ((_ model: SRShortModel?) -> Void)?
lazy var bgImageView = UIImageView(image: UIImage(named: "home_banner_bg"))
lazy var bannerView: FSPagerView = {
let view = FSPagerView()
view.delegate = self
view.dataSource = self
view.isInfinite = true
view.automaticSlidingInterval = 5
view.register(SRHomeBannerCell.self, forCellWithReuseIdentifier: "cell")
return view
}()
// lazy var miniBgView: UIView = {
// let view = UIView()
// view.layer.cornerRadius = 4
// view.layer.masksToBounds = true
// view.backgroundColor = ._051_B_22.withAlphaComponent(0.5)
// return view
// }()
lazy var miniCollectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = .init(width: 36, height: 46)
layout.minimumLineSpacing = 4
return layout
}()
lazy var miniCollectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: miniCollectionViewLayout)
collectionView.layer.cornerRadius = 4
collectionView.layer.masksToBounds = true
collectionView.backgroundColor = ._051_B_22.withAlphaComponent(0.5)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsVerticalScrollIndicator = false
collectionView.contentInset = .init(top: 5, left: 0, bottom: 5, right: 0)
collectionView.register(SRHomeBannerMiniCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
applyHeteromorphicLayer()
}
///
func applyHeteromorphicLayer() {
let viewSize = bannerView.bounds.size
let path = UIBezierPath()
let layer = CAShapeLayer()
//
let topRight = UIScreen.getRatioWidth(size: 25)
//
let bottomLeft = UIScreen.getRatioWidth(size: 10)
path.move(to: .init(x: 0, y: 0))
path.addLine(to: .init(x: viewSize.width - topRight, y: 0))
path.addLine(to: .init(x: viewSize.width, y: topRight))
path.addLine(to: .init(x: viewSize.width, y: viewSize.height))
path.addLine(to: .init(x: bottomLeft, y: viewSize.height))
path.addLine(to: .init(x: 0, y: viewSize.height - bottomLeft))
path.close()
layer.path = path.cgPath
bannerView.layer.mask = layer
}
}
extension SRHomeBannerView {
func sr_setupUI() {
let bgImage = self.bgImageView.image!
let bgWidth = UIScreen.width - 30
let bgHeight = bgImage.size.height / bgImage.size.width * bgWidth
addSubview(bgImageView)
addSubview(bannerView)
addSubview(miniCollectionView)
bgImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.top.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(bgHeight)
}
bannerView.snp.makeConstraints { make in
make.left.equalTo(bgImageView).offset(11)
make.right.equalTo(bgImageView).offset(-11)
make.top.equalTo(bgImageView).offset(18)
make.bottom.equalTo(bgImageView).offset(-12)
}
miniCollectionView.snp.makeConstraints { make in
make.left.equalTo(bannerView).offset(7)
make.centerY.equalTo(bannerView)
make.top.equalTo(bannerView).offset(7)
make.width.equalTo(46)
}
}
}
//MARK: FSPagerViewDelegate FSPagerViewDataSource
extension SRHomeBannerView: FSPagerViewDelegate, FSPagerViewDataSource {
func numberOfItems(in pagerView: FSPagerView) -> Int {
return self.dataArr?.count ?? 0
}
func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell {
let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "cell", at: index) as! SRHomeBannerCell
cell.model = self.dataArr?[index]
return cell
}
func pagerView(_ pagerView: FSPagerView, didSelectItemAt index: Int) {
self.didSelectedShort?(self.dataArr?[index])
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SRHomeBannerView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SRHomeBannerMiniCell
cell.model = self.dataArr?[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row == self.bannerView.currentIndex { return }
self.bannerView.selectItem(at: indexPath.row, animated: true)
}
}

View File

@ -0,0 +1,135 @@
//
// SRHomeBingeWorthyCell.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeBingeWorthyCell: UICollectionViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
titleLabel.text = model?.name
desLabel.text = model?.sr_description
if let text = model?.category?.first, text.count > 0 {
categoryLabel.text = "#\(text)"
} else {
categoryLabel.text = nil
}
}
}
lazy var bgImageView = UIImageView(image: UIImage(named: "home_binge_worthy_cell_image"))
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .white
return label
}()
lazy var categoryLabel: SRLabel = {
let label = SRLabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColor = .A_6_A_6_A_6
label.numberOfLines = 3
return label
}()
lazy var playImageView = UIImageView(image: UIImage(named: "play_icon_01"))
lazy var playLabel: UILabel = {
let label = SRLabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
label.text = "Play"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SRHomeBingeWorthyCell {
private func sr_setupUI() {
contentView.addSubview(bgImageView)
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(categoryLabel)
contentView.addSubview(desLabel)
contentView.addSubview(playImageView)
playImageView.addSubview(playLabel)
bgImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.top.equalToSuperview().offset(10)
make.width.equalTo(126)
make.height.equalTo(168)
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(11)
make.top.equalToSuperview().offset(27)
make.right.lessThanOrEqualToSuperview().offset(-15)
}
categoryLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(12)
make.right.lessThanOrEqualToSuperview().offset(-15)
}
desLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(43)
make.right.lessThanOrEqualToSuperview().offset(-15)
}
playImageView.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(11)
make.bottom.equalTo(coverImageView).offset(1)
}
playLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(45)
make.top.equalToSuperview().offset(9)
}
}
}

View File

@ -0,0 +1,75 @@
//
// SRHomeBingeWorthyView.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeBingeWorthyView: SRHomeModuleView {
var dataArr: [SRShortModel]? {
didSet {
collectionView.reloadData()
}
}
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 12
layout.itemSize = .init(width: 320, height: 192)
return layout
}()
lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 15, bottom: 0, right: 15)
collectionView.register(SRHomeBingeWorthyCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.titleLabel.text = "Binge-Worthy".localized
addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(button.snp.bottom).offset(6)
make.height.equalTo(collectionViewLayout.itemSize.height)
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//MARK: UICollectionViewDataSource UICollectionViewDelegate
extension SRHomeBingeWorthyView: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SRHomeBingeWorthyCell
cell.model = self.dataArr?[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.didSelectedShort?(self.dataArr?[indexPath.row])
}
}

View File

@ -0,0 +1,28 @@
//
// SRHomeChildCell.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRHomeChildCell: UICollectionViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
}
}
@IBOutlet weak var coverImageView: SRImageView!
override func awakeFromNib() {
super.awakeFromNib()
coverImageView.layer.cornerRadius = 2
coverImageView.layer.masksToBounds = true
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="24128" 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="24063"/>
<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="SRHomeChildCell" customModule="SynthReel" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="246" height="347"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="246" height="347"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Yvo-SQ-qpb" customClass="SRImageView" customModule="SynthReel" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="246" height="347"/>
</imageView>
</subviews>
</view>
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
<constraints>
<constraint firstItem="Yvo-SQ-qpb" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="1iZ-0m-azA"/>
<constraint firstAttribute="trailing" secondItem="Yvo-SQ-qpb" secondAttribute="trailing" id="alg-1k-1ZY"/>
<constraint firstAttribute="bottom" secondItem="Yvo-SQ-qpb" secondAttribute="bottom" id="fPg-nk-5hd"/>
<constraint firstItem="Yvo-SQ-qpb" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="wUJ-cX-k96"/>
</constraints>
<size key="customSize" width="246" height="347"/>
<connections>
<outlet property="coverImageView" destination="Yvo-SQ-qpb" id="DhO-6m-P2P"/>
</connections>
<point key="canvasLocation" x="96.18320610687023" y="179.22535211267606"/>
</collectionViewCell>
</objects>
</document>

View File

@ -0,0 +1,156 @@
//
// SRHomeHeaderView.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
import YYCategories
class SRHomeHeaderView: UIView {
var contentHeight: CGFloat {
return scrollView.contentSize.height + 1 + 20
}
var heightDidChange: (() -> Void)?
weak var viewModel: SRHomeViewModel?
lazy var scrollView: SRScrollView = {
let scrollView = SRScrollView()
scrollView.isScrollEnabled = false
scrollView.addObserver(self, forKeyPath: "contentSize", context: nil)
return scrollView
}()
lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 20
return view
}()
lazy var bannerView: SRHomeBannerView = {
let view = SRHomeBannerView()
view.didSelectedShort = { [weak self] model in
self?.pushShortDetail(model)
}
return view
}()
lazy var youLikeView: SRHomeYouLikeView = {
let view = SRHomeYouLikeView()
view.didSelectedShort = { [weak self] model in
self?.pushShortDetail(model)
}
return view
}()
lazy var topChartsView: SRHomeTopChartsView = {
let view = SRHomeTopChartsView()
view.didSelectedShort = { [weak self] model in
self?.pushShortDetail(model)
}
return view
}()
lazy var bingeWorthyView: SRHomeBingeWorthyView = {
let view = SRHomeBingeWorthyView()
view.didSelectedShort = { [weak self] model in
self?.pushShortDetail(model)
}
return view
}()
lazy var viralHitsView: SRHomeViralHitsView = {
let view = SRHomeViralHitsView()
view.didSelectedShort = { [weak self] model in
self?.pushShortDetail(model)
}
return view
}()
lazy var premiereNowView: SRHomePremiereNowView = {
let view = SRHomePremiereNowView()
view.didSelectedShort = { [weak self] model in
self?.pushShortDetail(model)
}
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(scrollView)
scrollView.addSubview(stackView)
scrollView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-20)
}
stackView.snp.makeConstraints { make in
make.left.centerX.top.bottom.equalToSuperview()
}
reloadData()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
self.heightDidChange?()
}
}
func reloadData() {
stackView.sr_removeAllArrangedSubview()
var popularArr: [SRShortModel]? = nil
var updatesArr: [SRShortModel]? = nil
self.viewModel?.moduleArr.forEach {
if $0.module_key == .banner {
bannerView.dataArr = $0.list
stackView.addArrangedSubview(bannerView)
} else if $0.module_key == .detailsRecommand {
youLikeView.dataArr = $0.list
stackView.addArrangedSubview(youLikeView)
} else if $0.module_key == .popular {
popularArr = $0.list
stackView.addArrangedSubview(topChartsView)
} else if $0.module_key == .updates {
updatesArr = $0.list
stackView.addArrangedSubview(topChartsView)
} else if $0.module_key == .bingeWorthy {
bingeWorthyView.dataArr = $0.list
stackView.addArrangedSubview(bingeWorthyView)
} else if $0.module_key == .viralHits {
viralHitsView.dataArr = $0.list
stackView.addArrangedSubview(viralHitsView)
} else if $0.module_key == .premiereNow {
premiereNowView.dataArr = $0.list
stackView.addArrangedSubview(premiereNowView)
}
}
self.topChartsView.popularArr = popularArr
self.topChartsView.updatesArr = updatesArr
self.topChartsView.reloadData()
}
func pushShortDetail(_ model: SRShortModel?) {
let vc = SRDetailPlayerViewController()
vc.shortId = model?.short_play_id
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,61 @@
//
// SRHomeHotView.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeHotView: UIView {
var count: Int = 0 {
didSet {
if count > 1000 {
countLabel.text = NSNumber(value: CGFloat(count) / 1000).toString(maximumFractionDigits: 1) + "k"
} else {
countLabel.text = "\(count)"
}
}
}
lazy var iconImageView = UIImageView(image: UIImage(named: "hot_icon_01"))
lazy var countLabel: SRLabel = {
let label = SRLabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColors = [UIColor._4_CFFD_4.cgColor, UIColor._51_D_4_FF.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(iconImageView)
addSubview(countLabel)
iconImageView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalToSuperview().offset(1)
make.centerY.equalToSuperview()
}
countLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview()
make.left.equalTo(iconImageView.snp.right).offset(3)
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,51 @@
//
// SRHomeMenuCell.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXSegmentedView
import SnapKit
class SRHomeMenuCell: JXSegmentedTitleGradientCell/*JXSegmentedTitleCell*/ {
lazy var bgImageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
// lazy var gTitleLabel: SRLabel = {
// let
// }()
override func commonInit() {
super.commonInit()
// self.titleLabel.removeFromSuperview()
// self.maskTitleLabel.removeFromSuperview()
self.contentView.addSubview(bgImageView)
bgImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
self.contentView.sendSubviewToBack(bgImageView)
}
override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) {
super.reloadData(itemModel: itemModel, selectedType: selectedType)
if itemModel.isSelected {
bgImageView.image = UIImage(named: "home_menu_bg_image_selected")
} else {
bgImageView.image = UIImage(named: "home_menu_bg_image")
}
}
}

View File

@ -0,0 +1,65 @@
//
// SRHomeModuleView.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeModuleView: UIView {
var didSelectedShort: ((_ model: SRShortModel?) -> Void)?
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 18, weight: .bold)
label.textColor = .white
return label
}()
lazy var indicatorImageView = UIImageView(image: UIImage(named: "arrow_right_icon_01"))
lazy var button: UIControl = {
let button = UIControl(frame: .zero, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
}))
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(button)
button.addSubview(titleLabel)
button.addSubview(indicatorImageView)
button.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.top.equalToSuperview()
make.height.equalTo(40)
}
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview()
}
indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,198 @@
//
// SRHomePremiereNowView.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomePremiereNowView: SRHomeModuleView {
var dataArr: [SRShortModel]? {
didSet {
let count = dataArr?.count ?? 0
self.oneModel = dataArr?.first
if count >= 2 {
self.twoModel = dataArr?[1]
} else {
self.twoModel = nil
}
if count >= 3 {
self.threeModel = dataArr?[2]
} else {
self.threeModel = nil
}
}
}
var oneModel: SRShortModel? {
didSet {
oneCoverImageView.sr_setImage(oneModel?.image_url)
oneTitleLabel.text = oneModel?.name
}
}
var twoModel: SRShortModel? {
didSet {
if let model = twoModel {
twoCoverImageView.sr_setImage(model.image_url)
twoCoverImageView.isHidden = false
} else {
twoCoverImageView.isHidden = true
}
}
}
var threeModel: SRShortModel? {
didSet {
if let model = threeModel {
threeCoverImageView.sr_setImage(model.image_url)
threeCoverImageView.isHidden = false
} else {
threeCoverImageView.isHidden = true
}
}
}
lazy var bgImageView = UIImageView(image: UIImage(named: "premiere_now_bg_image_01"))
lazy var oneBgImageView = UIImageView(image: UIImage(named: "premiere_now_bg_image_02"))
lazy var oneCoverImageView: SRImageView = {
let imageView = SRImageView()
imageView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer { [weak self] _ in
guard let self = self else { return }
self.didSelectedShort?(self.oneModel)
}
imageView.addGestureRecognizer(tap)
return imageView
}()
lazy var oneTitleBgView = UIImageView(image: UIImage(named: "premiere_now_title_bg"))
lazy var oneTitleLabel: UILabel = {
let label = SRLabel()
label.font = .font(ofSize: UIScreen.getRatioWidth(size: 12), weight: .medium)
label.numberOfLines = 2
label.textAlignment = .center
label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
return label
}()
lazy var twoCoverImageView: SRImageView = {
let imageView = SRImageView()
imageView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer { [weak self] _ in
guard let self = self else { return }
self.didSelectedShort?(self.twoModel)
}
imageView.addGestureRecognizer(tap)
return imageView
}()
lazy var threeCoverImageView: SRImageView = {
let imageView = SRImageView()
imageView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer { [weak self] _ in
guard let self = self else { return }
self.didSelectedShort?(self.threeModel)
}
imageView.addGestureRecognizer(tap)
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.titleLabel.text = "Premiere Now".localized
sr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
oneCoverImageView.applyHexagonMask(2)
twoCoverImageView.applyHexagonMask(2)
threeCoverImageView.applyHexagonMask(2)
}
}
extension SRHomePremiereNowView {
private func sr_setupUI() {
oneBgImageView.isUserInteractionEnabled = true
addSubview(bgImageView)
addSubview(oneBgImageView)
oneBgImageView.addSubview(oneCoverImageView)
oneCoverImageView.addSubview(oneTitleBgView)
oneTitleBgView.addSubview(oneTitleLabel)
addSubview(twoCoverImageView)
addSubview(threeCoverImageView)
bgImageView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.centerY.equalTo(oneBgImageView)
make.height.equalTo(UIScreen.getRatioWidth(size: 155))
}
oneBgImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview()
make.top.equalTo(button.snp.bottom).offset(8)
make.width.equalTo(UIScreen.getRatioWidth(size: 200))
make.height.equalTo(UIScreen.getRatioWidth(size: 175))
}
oneCoverImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalTo(UIScreen.getRatioWidth(size: 190))
make.height.equalTo(UIScreen.getRatioWidth(size: 166))
}
oneTitleBgView.snp.makeConstraints { make in
make.bottom.equalToSuperview()
make.left.equalToSuperview().offset(UIScreen.getRatioWidth(size: 24.5))
make.right.equalToSuperview().offset(-UIScreen.getRatioWidth(size: 27))
make.height.equalTo(UIScreen.getRatioWidth(size: 41))
}
oneTitleLabel.snp.makeConstraints { make in
make.centerY.equalTo(oneTitleBgView.snp.top).offset(UIScreen.getRatioWidth(size: 23))
make.centerX.equalToSuperview().offset(UIScreen.getRatioWidth(size: 1.5))
make.right.lessThanOrEqualToSuperview().offset(-UIScreen.getRatioWidth(size: 15))
}
twoCoverImageView.snp.makeConstraints { make in
make.centerY.equalTo(oneBgImageView)
make.left.equalToSuperview().offset(UIScreen.getRatioWidth(size: 20))
make.width.equalTo(UIScreen.getRatioWidth(size: 62))
make.height.equalTo(UIScreen.getRatioWidth(size: 54))
}
threeCoverImageView.snp.makeConstraints { make in
make.centerY.equalTo(oneBgImageView)
make.right.equalToSuperview().offset(-UIScreen.getRatioWidth(size: 20))
make.width.height.equalTo(twoCoverImageView)
}
}
}

View File

@ -0,0 +1,196 @@
//
// SRHomeTopChartsContentView.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
extension SRHomeTopChartsView {
class ContentView: UIView {
var didSelectedShort: ((_ model: SRShortModel?) -> Void)?
var dataArr: [SRShortModel]? {
didSet {
self.collectionView.reloadData()
}
}
lazy var bgImageView = UIImageView()
lazy var trophyImageView = UIImageView(image: UIImage(named: "trophy_icon_01"))
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 15, weight: .bold)
label.textColor = .white
return label
}()
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = .init(width: 198, height: 53)
layout.minimumLineSpacing = 9
return layout
}()
lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isScrollEnabled = false
collectionView.register(ContentCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
bgImageView.isUserInteractionEnabled = true
addSubview(bgImageView)
addSubview(trophyImageView)
bgImageView.addSubview(titleLabel)
bgImageView.addSubview(collectionView)
bgImageView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(16)
make.width.equalTo(198)
}
trophyImageView.snp.makeConstraints { make in
make.top.right.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.top.equalToSuperview().offset(13)
}
collectionView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.top.equalToSuperview().offset(46)
make.bottom.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SRHomeTopChartsView.ContentView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return min(3, self.dataArr?.count ?? 0)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SRHomeTopChartsView.ContentCell
cell.row = indexPath.row
cell.model = self.dataArr?[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.didSelectedShort?(self.dataArr?[indexPath.row])
}
}
extension SRHomeTopChartsView {
class ContentCell: UICollectionViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
titleLabel.text = model?.name
hotView.count = model?.watch_total ?? 0
}
}
var row: Int = 0 {
didSet {
numLabel.text = "\(row + 1)"
}
}
lazy var numLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .bold)
label.textColor = .white
return label
}()
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 2
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .medium)
label.textColor = .white
return label
}()
lazy var hotView: SRHomeHotView = {
let view = SRHomeHotView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(numLabel)
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(hotView)
numLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.centerX.equalTo(self.contentView.snp.left).offset(14)
}
coverImageView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.left.equalToSuperview().offset(24)
make.width.equalTo(40)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.left.equalTo(coverImageView.snp.right).offset(6)
make.right.lessThanOrEqualToSuperview().offset(-9)
}
hotView.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(6)
make.bottom.equalToSuperview().offset(-7)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}

View File

@ -0,0 +1,106 @@
//
// SRHomeTopChartsView.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeTopChartsView: SRHomeModuleView {
var popularArr: [SRShortModel]? {
didSet {
popularView.dataArr = popularArr
}
}
var updatesArr: [SRShortModel]? {
didSet {
updatesView.dataArr = updatesArr
}
}
lazy var scrollView: SRScrollView = {
let scrollView = SRScrollView()
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.spacing = 16
return view
}()
lazy var popularView: ContentView = {
let view = ContentView()
view.titleLabel.text = "Popular".localized
view.bgImageView.image = UIImage(named: "popular_bg_image")
view.didSelectedShort = { [weak self] model in
self?.didSelectedShort?(model)
}
return view
}()
lazy var updatesView: ContentView = {
let view = ContentView()
view.titleLabel.text = "Updates".localized
view.bgImageView.image = UIImage(named: "updates_bg_image")
view.didSelectedShort = { [weak self] model in
self?.didSelectedShort?(model)
}
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.titleLabel.text = "Top Charts".localized
sr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reloadData() {
stackView.sr_removeAllArrangedSubview()
if let arr = self.popularArr, arr.count > 0 {
stackView.addArrangedSubview(popularView)
}
if let arr = self.updatesArr, arr.count > 0 {
stackView.addArrangedSubview(updatesView)
}
}
}
extension SRHomeTopChartsView {
private func sr_setupUI() {
addSubview(scrollView)
scrollView.addSubview(stackView)
scrollView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(self.button.snp.bottom)
make.height.equalTo(238 + 16)
make.bottom.equalToSuperview()
}
stackView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.height.equalTo(238 + 16)
}
}
}

View File

@ -0,0 +1,86 @@
//
// SRHomeViralHitsCell.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeViralHitsCell: UICollectionViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
titleLabel.text = model?.name
hotView.count = model?.watch_total ?? 0
}
}
lazy var bgImageView = UIImageView(image: UIImage(named: "home_viral_hits_cell_image"))
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 2
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .medium)
label.textColor = .white
return label
}()
lazy var hotView: SRHomeHotView = {
let view = SRHomeHotView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SRHomeViralHitsCell {
private func sr_setupUI() {
contentView.addSubview(bgImageView)
contentView.addSubview(coverImageView)
coverImageView.addSubview(hotView)
contentView.addSubview(titleLabel)
bgImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
coverImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.left.equalToSuperview().offset(13)
make.top.equalToSuperview().offset(21)
make.height.equalTo(180)
}
hotView.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-5)
make.top.equalToSuperview().offset(4)
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(13)
make.right.lessThanOrEqualToSuperview().offset(-13)
make.top.equalTo(coverImageView.snp.bottom).offset(4)
}
}
}

View File

@ -0,0 +1,74 @@
//
// SRHomeViralHitsView.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeViralHitsView: SRHomeModuleView {
var dataArr: [SRShortModel]? {
didSet {
collectionView.reloadData()
}
}
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 12
layout.itemSize = .init(width: 161, height: 237)
return layout
}()
lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 15, bottom: 0, right: 15)
collectionView.register(SRHomeViralHitsCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.titleLabel.text = "Viral Hits".localized
addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(button.snp.bottom).offset(6)
make.height.equalTo(collectionViewLayout.itemSize.height)
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SRHomeViralHitsView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SRHomeViralHitsCell
cell.model = dataArr?[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.didSelectedShort?(self.dataArr?[indexPath.row])
}
}

View File

@ -0,0 +1,120 @@
//
// SRHomeYouLikeCell.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeYouLikeCell: UICollectionViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
titleLabel.text = model?.name
desLabel.text = model?.sr_description
}
}
lazy var bgView: UIView = UIImageView(image: UIImage(named: "cell_bg_image_01"))
lazy var coverBgView: UIView = {
let view = UIView()
view.layer.cornerRadius = 34
view.layer.masksToBounds = true
view.layer.borderWidth = 0.7
view.layer.borderColor = UIColor._4_CFFD_4.withAlphaComponent(0.25).cgColor
return view
}()
lazy var coverBorderView: SRGradientView = {
let view = SRGradientView()
view.layer.cornerRadius = 31
view.layer.masksToBounds = true
view.colors = [UIColor._4_CFFD_4.cgColor, UIColor._51_D_4_FF.cgColor]
view.startPoint = .init(x: 0, y: 0)
view.endPoint = .init(x: 1, y: 1)
return view
}()
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 30.25
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .white
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColor = .A_6_A_6_A_6
label.numberOfLines = 2
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SRHomeYouLikeCell {
private func sr_setupUI() {
contentView.addSubview(bgView)
bgView.addSubview(coverBgView)
coverBgView.addSubview(coverBorderView)
coverBgView.addSubview(coverImageView)
bgView.addSubview(titleLabel)
bgView.addSubview(desLabel)
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
coverBgView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(8)
make.width.height.equalTo(68)
}
coverBorderView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(62)
}
coverImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(60.5)
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(coverBgView.snp.right).offset(9)
make.top.equalToSuperview().offset(17)
make.right.lessThanOrEqualToSuperview().offset(-12)
}
desLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(11)
make.right.lessThanOrEqualToSuperview().offset(-12)
}
}
}

View File

@ -0,0 +1,81 @@
//
// SRHomeYouLikeView.swift
// SynthReel
//
// Created by on 2025/11/15.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeYouLikeView: UIView {
var dataArr: [SRShortModel]? {
didSet {
self.collectionView.reloadData()
}
}
var didSelectedShort: ((_ model: SRShortModel?) -> Void)?
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 14
layout.itemSize = .init(width: 220, height: 82)
return layout
}()
lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 15, bottom: 0, right: 15)
collectionView.register(SRHomeYouLikeCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SRHomeYouLikeView {
func sr_setupUI() {
addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.left.right.top.bottom.equalToSuperview()
make.height.equalTo(82)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SRHomeYouLikeView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SRHomeYouLikeCell
cell.model = self.dataArr?[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.didSelectedShort?(self.dataArr?[indexPath.row])
}
}

View File

@ -0,0 +1,114 @@
//
// SRHotSearchCell.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHotSearchCell: SRTableViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
titleLabel.text = model?.name
hotView.count = model?.watch_total ?? 0
desLabel.text = model?.sr_description
}
}
var row: Int = 0 {
didSet {
numLabel.text = "\(row + 1)"
}
}
lazy var numLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .bold)
label.textColor = .white
return label
}()
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 2
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .white
return label
}()
lazy var hotView: SRHomeHotView = {
let view = SRHomeHotView()
return view
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColor = .A_6_A_6_A_6
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
sr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SRHotSearchCell {
private func sr_setupUI() {
contentView.addSubview(numLabel)
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(hotView)
contentView.addSubview(desLabel)
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(24)
make.width.equalTo(40)
make.height.equalTo(53)
make.top.equalToSuperview().offset(8)
make.bottom.equalToSuperview().offset(-8)
}
numLabel.snp.makeConstraints { make in
make.centerX.equalTo(self.contentView.snp.left).offset(14)
make.centerY.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(6)
make.top.equalTo(coverImageView).offset(8)
}
hotView.snp.makeConstraints { make in
make.left.equalTo(titleLabel.snp.right).offset(15)
make.right.lessThanOrEqualToSuperview().offset(-30)
make.centerY.equalTo(titleLabel)
}
desLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalTo(coverImageView).offset(-6)
make.right.equalToSuperview().offset(-27)
}
}
}

View File

@ -0,0 +1,115 @@
//
// SRHotSearchView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
import YYCategories
class SRHotSearchView: UIView {
override var intrinsicContentSize: CGSize {
return .init(width: UIScreen.width, height: UIScreen.height)
}
var dataArr: [SRShortModel] = [] {
didSet {
self.tableView.reloadData()
}
}
lazy var bgImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "search_hot_bg_image"))
imageView.isUserInteractionEnabled = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = SRLabel()
label.font = .font(ofSize: 15, weight: .semibold)
label.textColors = [UIColor._7_AF_4_E_0.cgColor, UIColor.white.cgColor, UIColor._7_AF_4_E_0.cgColor]
label.textStartPoint = .init(x: 0, y: 0.5)
label.textEndPoint = .init(x: 1, y: 0.5)
label.text = "Premium Picks".localized
return label
}()
lazy var tableView: SRTableView = {
let tableView = SRTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.keyboardDismissMode = .onDrag
tableView.contentInset = .init(top: 0, left: 0, bottom: 5, right: 0)
tableView.register(SRHotSearchCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SRHotSearchView {
private func sr_setupUI() {
addSubview(bgImageView)
bgImageView.addSubview(titleLabel)
bgImageView.addSubview(tableView)
bgImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.centerX.equalToSuperview()
make.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-(UIScreen.safeBottom + 10))
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.top.equalToSuperview().offset(13)
}
tableView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(38)
make.bottom.equalToSuperview()
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension SRHotSearchView: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SRHotSearchCell
cell.row = indexPath.row
cell.model = dataArr[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataArr.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let model = dataArr[indexPath.row]
let vc = SRDetailPlayerViewController()
vc.shortId = model.short_play_id
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,93 @@
//
// SRSearchHomeView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRSearchHomeView: UIView {
var didSearch: ((_ text: String) -> Void)?
weak var viewModel: SRSearchViewModel? {
didSet {
viewModel?.addObserver(self, forKeyPath: "hotDataArr", context: nil)
viewModel?.addObserver(self, forKeyPath: "recordList", context: nil)
self.hotView.dataArr = self.viewModel?.hotDataArr ?? []
self.recordView.dataArr = self.viewModel?.recordList ?? []
updateLayout()
}
}
lazy var stackView: UIStackView = {
let view = UIStackView()
view.spacing = 20
view.axis = .vertical
return view
}()
lazy var recordView: SRSearchRecordView = {
let view = SRSearchRecordView()
view.didSearch = { [weak self] text in
self?.didSearch?(text)
}
view.didDeleteAll = { [weak self] in
self?.viewModel?.clearSearchRecord()
}
return view
}()
lazy var hotView: SRHotSearchView = {
let view = SRHotSearchView()
return view
}()
deinit {
viewModel?.removeObserver(self, forKeyPath: "hotDataArr")
viewModel?.removeObserver(self, forKeyPath: "recordList")
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(stackView)
stackView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(14)
make.bottom.lessThanOrEqualToSuperview()
}
}
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 == "hotDataArr" {
self.hotView.dataArr = self.viewModel?.hotDataArr ?? []
} else if keyPath == "recordList" {
self.recordView.dataArr = self.viewModel?.recordList ?? []
}
updateLayout()
}
func updateLayout() {
stackView.sr_removeAllArrangedSubview()
if self.recordView.dataArr.count > 0 {
stackView.addArrangedSubview(recordView)
}
if self.hotView.dataArr.count > 0 {
stackView.addArrangedSubview(self.hotView)
}
}
}

View File

@ -0,0 +1,42 @@
//
// SRSearchRecordCell.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRSearchRecordCell: UICollectionViewCell {
static let TextFont: UIFont = .font(ofSize: 12, weight: .regular)
lazy var textLabel: UILabel = {
let label = UILabel()
label.font = Self.TextFont
label.textColor = .A_6_A_6_A_6
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = ._010101.withAlphaComponent(0.2)
contentView.layer.cornerRadius = 14
contentView.layer.masksToBounds = true
contentView.layer.borderWidth = 1
contentView.layer.borderColor = UIColor._4_CFFD_4.withAlphaComponent(0.25).cgColor
contentView.addSubview(textLabel)
textLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,135 @@
//
// SRSearchRecordView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
import collection_view_layouts
class SRSearchRecordView: UIView {
var didSearch: ((_ text: String) -> Void)?
var didDeleteAll: (() -> Void)?
var dataArr: [String] = [] {
didSet {
self.collectionView.reloadData()
}
}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .medium)
label.textColor = .CCCCCC
label.text = "Search History".localized
return label
}()
lazy var deleteButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.didDeleteAll?()
}))
button.setImage(UIImage(named: "delete_icon_01"), for: .normal)
return button
}()
private lazy var collectionViewLayout: TagsLayout = {
let layout = TagsLayout()
layout.delegate = self
layout.contentPadding = ItemsPadding(horizontal: 15, vertical: 0)
layout.cellsPadding = ItemsPadding(horizontal: 9, vertical: 10)
return layout
}()
private lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
collectionView.register(SRSearchRecordCell.self, forCellWithReuseIdentifier: "tagCell")
return collectionView
}()
deinit {
self.collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
let height = self.collectionView.contentSize.height
self.collectionView.snp.updateConstraints { make in
make.height.equalTo(height + 1)
}
}
}
}
extension SRSearchRecordView {
private func sr_setupUI() {
addSubview(titleLabel)
addSubview(deleteButton)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(14)
make.centerY.equalTo(deleteButton)
}
deleteButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-5)
make.top.equalToSuperview().offset(-10)
make.width.equalTo(36)
make.height.equalTo(36)
}
collectionView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(24)
make.left.equalToSuperview()
make.right.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(1)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SRSearchRecordView: UICollectionViewDelegate, UICollectionViewDataSource {
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! SRSearchRecordCell
cell.textLabel.text = dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.didSearch?(dataArr[indexPath.row])
}
}
extension SRSearchRecordView: LayoutDelegate {
func cellSize(indexPath: IndexPath) -> CGSize {
let text = dataArr[indexPath.row]
let size = text.size(SRSearchRecordCell.TextFont, .init(width: UIScreen.width, height: 24))
return .init(width: size.width + 20, height: 28)
}
}

View File

@ -0,0 +1,139 @@
//
// SRSearchResultCell.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRSearchResultCell: UICollectionViewCell {
var model: SRShortModel? {
didSet {
coverImageView.sr_setImage(model?.image_url)
titleLabel.text = model?.name
desLabel.text = model?.sr_description
if let category = model?.categoryList?.first?.name, category.count > 0 {
categoryLabel.text = "#\(category)"
} else {
categoryLabel.text = ""
}
}
}
lazy var bgImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "search_result_cell_bg_image"))
return imageView
}()
lazy var coverImageView: SRImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 4
imageView.layer.masksToBounds = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .white
return label
}()
lazy var categoryLabel: SRLabel = {
let label = SRLabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColor = .A_6_A_6_A_6
label.numberOfLines = 3
return label
}()
lazy var playImageView = UIImageView(image: UIImage(named: "play_icon_01"))
lazy var playLabel: UILabel = {
let label = SRLabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
label.text = "Play"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SRSearchResultCell {
private func sr_setupUI() {
contentView.addSubview(bgImageView)
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(categoryLabel)
contentView.addSubview(desLabel)
contentView.addSubview(playImageView)
playImageView.addSubview(playLabel)
bgImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
coverImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.centerY.equalToSuperview()
make.width.equalTo(126)
make.height.equalTo(168)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(coverImageView).offset(17)
make.left.equalTo(coverImageView.snp.right).offset(10)
make.right.lessThanOrEqualToSuperview().offset(-13)
}
categoryLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(12)
}
desLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(43)
make.right.lessThanOrEqualToSuperview().offset(-12)
}
playImageView.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalTo(coverImageView)
}
playLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(45)
make.top.equalToSuperview().offset(9)
}
}
}

View File

@ -0,0 +1,111 @@
//
// SRSearchResultView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
import YYCategories
class SRSearchResultView: UIView {
weak var viewModel: SRSearchViewModel?
lazy var dataArr: [SRShortModel] = []
lazy var titleLabel: UILabel = {
let label = SRLabel()
label.font = .font(ofSize: 15, weight: .semibold)
label.textColors = [UIColor._7_AF_4_E_0.cgColor, UIColor.white.cgColor, UIColor._7_AF_4_E_0.cgColor]
label.textStartPoint = .init(x: 0, y: 0.5)
label.textEndPoint = .init(x: 1, y: 0.5)
label.text = "Search Results".localized
return label
}()
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = .init(width: UIScreen.width - 30, height: 192)
layout.minimumLineSpacing = 10
return layout
}()
lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.keyboardDismissMode = .onDrag
collectionView.showsVerticalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
collectionView.register(SRSearchResultCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func search(_ text: String) {
self.dataArr.removeAll()
self.collectionView.reloadData()
Task {
if let arr = await SRHomeApi.requestSearch(text) {
self.dataArr = arr
self.collectionView.reloadData()
}
}
}
}
extension SRSearchResultView {
private func sr_setupUI() {
addSubview(titleLabel)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.top.equalToSuperview().offset(30)
}
collectionView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.bottom.equalToSuperview()
make.top.equalToSuperview().offset(59)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SRSearchResultView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SRSearchResultCell
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 = SRDetailPlayerViewController()
vc.shortId = model.short_play_id
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}

View File

@ -0,0 +1,125 @@
//
// SRSearchTextView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRSearchTextView: UIView {
var didSearch: ((_ text: String) -> Void)?
var text: String? {
get {
return textField.text
}
set {
textField.text = newValue
}
}
lazy var bgImageView = UIImageView(image: UIImage(named: "search_text_bg_image"))
lazy var button: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.background.cornerRadius = 0
configuration.background.image = UIImage(named: "search_text_button")
configuration.contentInsets = .init(top: 0, leading: 10, bottom: 0, trailing: 10)
configuration.attributedTitle = AttributedString("Search".localized, attributes: AttributeContainer([
.font : UIFont.font(ofSize: 14, weight: .medium),
.foregroundColor : UIColor._010101
]))
let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.handleSearch()
}))
button.setContentHuggingPriority(.required, for: .horizontal)
button.setContentCompressionResistancePriority(.required, for: .horizontal)
return button
}()
lazy var textField: UITextField = {
let textField = UITextField(frame: .zero)
textField.tintColor = UIColor.white.withAlphaComponent(0.5)
textField.delegate = self
textField.returnKeyType = .search
textField.font = .font(ofSize: 12, weight: .medium)
textField.textColor = .white
textField.attributedPlaceholder = NSAttributedString(string: "search_placeholder_text".localized, attributes: [
.font : UIFont.font(ofSize: 12, weight: .medium),
.foregroundColor : UIColor.white.withAlphaComponent(0.3)
])
return textField
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@discardableResult
override func becomeFirstResponder() -> Bool {
super.becomeFirstResponder()
return self.textField.becomeFirstResponder()
}
@discardableResult
override func resignFirstResponder() -> Bool {
super.resignFirstResponder()
return self.textField.resignFirstResponder()
}
private func handleSearch() {
if let text = textField.text {
self.didSearch?(text)
}
}
}
extension SRSearchTextView {
private func sr_setupUI() {
addSubview(bgImageView)
addSubview(button)
addSubview(textField)
bgImageView.snp.makeConstraints { make in
make.top.bottom.left.equalToSuperview()
make.right.equalToSuperview().offset(-8)
make.height.equalTo(40)
}
button.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.right.equalToSuperview()
}
textField.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.left.equalToSuperview().offset(14)
make.right.equalTo(button.snp.left).offset(-15)
}
}
}
//MARK: UITextFieldDelegate
extension SRSearchTextView: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
handleSearch()
return true
}
}

View File

@ -0,0 +1,143 @@
//
// SRHomeChildViewController.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRHomeChildViewController: SRViewController {
var categoryId: String?
var dataArr: [SRShortModel] = []
var page = 1
lazy var bgImageView = UIImageView(image: UIImage(named: "home_list_bg_image"))
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let itemWidth = floor((UIScreen.width - 30 - 26 - 10) / 2)
let itemHeight = 206.0 / 155.0 * itemWidth
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 10
layout.minimumLineSpacing = 10
layout.itemSize = .init(width: itemWidth, height: itemHeight)
layout.sectionInset = .init(top: 0, left: 13 + 15, bottom: 0, right: 13 + 15)
return layout
}()
lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.register(UINib(nibName: "SRHomeChildCell", bundle: nil), forCellWithReuseIdentifier: "cell")
collectionView.sr_addRefreshFooter { [weak self] in
self?.handleFooterRefresh(nil)
}
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .clear
self.backgroundImageView.isHidden = true
sr_setupUI()
Task {
await requestDataArr(page: self.page)
}
}
override func listScrollView() -> UIScrollView {
return collectionView
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
Task {
await self.requestDataArr(page: 1)
completer?()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
Task {
await self.requestDataArr(page: self.page + 1)
self.collectionView.sr_endFooterRefreshing()
completer?()
}
}
}
extension SRHomeChildViewController {
private func sr_setupUI() {
view.addSubview(bgImageView)
view.addSubview(collectionView)
bgImageView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.left.equalToSuperview().offset(15)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-15)
}
collectionView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(bgImageView).offset(30)
make.bottom.equalTo(bgImageView).offset(-27)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SRHomeChildViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SRHomeChildCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.didScrollCallback?(scrollView)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.row]
let vc = SRDetailPlayerViewController()
vc.shortId = model.short_play_id
self.navigationController?.pushViewController(vc, animated: true)
}
}
extension SRHomeChildViewController {
private func requestDataArr(page: Int) async {
guard let id = categoryId else { return }
if let dataArr = await SRHomeApi.requestCategoryVideoData(id, page: page) {
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += dataArr
self.page = page
self.collectionView.reloadData()
}
}
}

View File

@ -0,0 +1,279 @@
//
// SRHomeViewController.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
import YYCategories
import JXPagingView
import JXSegmentedView
extension JXPagingListContainerView: @retroactive JXSegmentedViewListContainer { }
class SRHomeViewController: SRViewController {
var viewModel = SRHomeViewModel()
lazy var searchButton: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.image = UIImage(named: "search_icon_01")
configuration.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 15)
let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let vc = SRSearchViewController()
self.navigationController?.pushViewController(vc, animated: true)
}))
return button
}()
lazy var titleImageView = UIImageView(image: UIImage(named: "home_title_image"))
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: UIScreen.getRatioWidth(size: 20), weight: .bold).withBoldItalic()
label.textColor = .white
label.text = "Drama Center".localized
return label
}()
lazy var menuContentView: UIView = {
let view = UIView()
return view
}()
lazy var moreButton: UIControl = {
let view = UIControl()
return view
}()
lazy var moreTitleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 18, weight: .bold)
label.textColor = .white
label.text = "Categories".localized
return label
}()
lazy var moreIconImageView = UIImageView(image: UIImage(named: "arrow_right_icon_01"))
lazy var menuDataSource: JXSegmentedTitleGradientDataSource = {
let dataSource = SRHomeMenuDataSource()
dataSource.titleNormalGradientColors = [UIColor.white.cgColor, UIColor._96_E_5_FF.cgColor, UIColor.white.cgColor]
dataSource.titleSelectedGradientColors = [UIColor.white.cgColor, UIColor._96_E_5_FF.cgColor, UIColor.white.cgColor]
dataSource.titleGradientStartPoint = .init(x: 0.5, y: 0)
dataSource.titleGradientEndPoint = .init(x: 0.5, y: 1)
dataSource.titleNormalFont = .font(ofSize: 13, weight: .medium)
dataSource.titleSelectedFont = .font(ofSize: 13, weight: .medium)
dataSource.itemWidth = 100
dataSource.itemWidthIncrement = 5
dataSource.titleNumberOfLines = 2
dataSource.itemSpacing = 5
return dataSource
}()
lazy var menuView: JXSegmentedView = {
let view = JXSegmentedView()
view.dataSource = menuDataSource
view.contentEdgeInsetLeft = 15
view.contentEdgeInsetRight = 15
return view
}()
lazy var pageView: JXPagingView = {
let view = JXPagingView(delegate: self)
view.layer.masksToBounds = true
view.mainTableView.backgroundColor = .clear
view.listContainerView.listCellBackgroundColor = .clear
view.mainTableView.gestureDelegate = self
view.automaticallyDisplayListVerticalScrollIndicator = false
view.mainTableView.sr_addRefreshHeader { [weak self] in
self?.handleHeaderRefresh(nil)
}
return view
}()
lazy var headerView: SRHomeHeaderView = {
let view = SRHomeHeaderView()
view.viewModel = self.viewModel
view.heightDidChange = { [weak self] in
guard let self = self else { return }
self.pageView.resizeTableHeaderViewHeight(animatable: false)
}
return view
}()
@MainActor
deinit {
srPrint(message: "销毁")
}
override func viewDidLoad() {
super.viewDidLoad()
sr_setupUI()
Task {
await requestModuleList()
await requestCategoryList()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
Task {
await requestModuleList()
self.pageView.mainTableView.sr_endHeaderRefreshing()
}
if let vc = self.pageView.listContainerView.validListDict[self.pageView.listContainerView.currentIndex] as? SRViewController {
vc.handleHeaderRefresh(nil)
}
}
}
extension SRHomeViewController {
func sr_setupUI() {
view.addSubview(searchButton)
view.addSubview(titleImageView)
titleImageView.addSubview(titleLabel)
view.addSubview(pageView)
menuContentView.addSubview(menuView)
menuContentView.addSubview(moreButton)
moreButton.addSubview(moreTitleLabel)
moreButton.addSubview(moreIconImageView)
menuView.listContainer = pageView.listContainerView
searchButton.snp.makeConstraints { make in
make.height.equalTo(44)
make.right.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.safeTop)
}
titleImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.top.equalToSuperview().offset(UIScreen.safeTop + 12)
make.width.equalTo(UIScreen.getRatioWidth(size: titleImageView.image?.size.width ?? 0))
make.height.equalTo(UIScreen.getRatioWidth(size: titleImageView.image?.size.height ?? 0))
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(UIScreen.getRatioWidth(size: 15))
make.centerY.equalTo(titleImageView.snp.top).offset(UIScreen.getRatioWidth(size: 21))
}
pageView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(titleImageView.snp.bottom).offset(0)
}
menuView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(40)
}
moreButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.right.equalToSuperview().offset(-15)
make.height.equalTo(40)
make.top.equalToSuperview()
}
moreTitleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview()
}
moreIconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview()
}
}
}
//MARK: JXPagingSmoothViewDataSource
extension SRHomeViewController: JXPagingViewDelegate {
func tableHeaderViewHeight(in pagingView: JXPagingView) -> Int {
return Int(ceil(self.headerView.contentHeight))
}
func tableHeaderView(in pagingView: JXPagingView) -> UIView {
return self.headerView
}
func heightForPinSectionHeader(in pagingView: JXPagingView) -> Int {
return 90
}
func viewForPinSectionHeader(in pagingView: JXPagingView) -> UIView {
return self.menuContentView
}
func numberOfLists(in pagingView: JXPagingView) -> Int {
return self.menuDataSource.titles.count
}
func pagingView(_ pagingView: JXPagingView, initListAtIndex index: Int) -> any JXPagingViewListViewDelegate {
let vc = SRHomeChildViewController()
vc.categoryId = self.viewModel.categoryArr[index].id
return vc
}
}
//MARK: JXPagingMainTableViewGestureDelegate
extension SRHomeViewController: JXPagingMainTableViewGestureDelegate {
func mainTableViewGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer == menuView.collectionView.panGestureRecognizer {
return false
}
if let view = otherGestureRecognizer.view {
var superview: UIView? = view.superview
while superview != nil {
if superview?.isKind(of: SRHomeHeaderView.self) == true {
return false
}
superview = superview?.superview
}
}
return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
}
}
extension SRHomeViewController {
func requestCategoryList() async {
await self.viewModel.requestCategoryList()
self.menuDataSource.titles = self.viewModel.categoryTitleArr
self.pageView.reloadData()
self.menuView.reloadData()
}
func requestModuleList() async {
await self.viewModel.requestModuleList()
self.headerView.reloadData()
}
}

View File

@ -0,0 +1,113 @@
//
// SRSearchViewController.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRSearchViewController: SRViewController {
lazy var viewModel = SRSearchViewModel()
lazy var returnButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.sr_handleNavigationBack()
}))
button.setImage(UIImage(named: "arrow_left_icon_01"), for: .normal)
return button
}()
lazy var textView: SRSearchTextView = {
let view = SRSearchTextView()
view.didSearch = { [weak self] text in
self?.search(text)
}
return view
}()
lazy var homeView: SRSearchHomeView = {
let view = SRSearchHomeView()
view.viewModel = self.viewModel
view.didSearch = { [weak self] text in
self?.textView.text = text
self?.search(text)
}
return view
}()
lazy var resultView: SRSearchResultView = {
let view = SRSearchResultView()
view.viewModel = self.viewModel
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
sr_setupUI()
self.resultView.isHidden = true
Task {
await self.viewModel.requestHotSearchData()
}
}
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 {
textView.resignFirstResponder()
homeView.isHidden = true
resultView.isHidden = false
resultView.search(text)
self.viewModel.addSearchRecord(text)
}
}
}
extension SRSearchViewController {
private func sr_setupUI() {
view.addSubview(returnButton)
view.addSubview(textView)
view.addSubview(homeView)
view.addSubview(resultView)
returnButton.snp.makeConstraints { make in
make.width.height.equalTo(44)
make.left.equalToSuperview().offset(10)
make.top.equalToSuperview().offset(UIScreen.safeTop)
}
textView.snp.makeConstraints { make in
make.left.equalTo(returnButton.snp.right).offset(10)
make.right.equalToSuperview().offset(-15)
make.centerY.equalTo(returnButton)
}
homeView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.bottom.equalToSuperview()
make.top.equalTo(returnButton.snp.bottom)
}
resultView.snp.makeConstraints { make in
make.edges.equalTo(homeView)
}
}
}

View File

@ -0,0 +1,36 @@
//
// SRHomeMenuDataSource.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXSegmentedView
class SRHomeMenuDataSource: JXSegmentedTitleGradientDataSource {
nonisolated override init() {
super.init()
}
nonisolated override func registerCellClass(in segmentedView: JXSegmentedView) {
MainActor.assumeIsolated {
segmentedView.collectionView.register(SRHomeMenuCell.self, forCellWithReuseIdentifier: "SRHomeMenuCell")
}
}
nonisolated override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell {
return MainActor.assumeIsolated {
let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "SRHomeMenuCell", at: index)
return cell
}
}
}

View File

@ -0,0 +1,107 @@
//
// SRHomeViewModel.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRHomeViewModel: NSObject {
lazy var categoryArr: [SRCategoryModel] = []
lazy var categoryTitleArr: [String] = []
lazy var moduleArr: [SRHomeModuleItem] = []
func requestCategoryList() async {
if let list = await SRHomeApi.requestCategoryList() {
self.categoryArr = list
var titles: [String] = []
self.categoryArr.forEach {
titles.append($0.name ?? "")
}
self.categoryTitleArr = titles
}
}
func requestModuleList() async {
guard let list = await SRHomeApi.requestHomeModulesData() else { return }
/*
get_details_recommand
popular = home_v3_recommand
updates = week_ranking
Binge-Worthy = week_highest_recommend
Viral Hits = highest_payment_hot_video
Premiere Now = new_recommand
*/
var bannerItem: SRHomeModuleItem? = nil
var detailsRecommandItem: SRHomeModuleItem? = nil
var popularItem: SRHomeModuleItem? = nil
var updatesItem: SRHomeModuleItem? = nil
var bingeWorthyItem: SRHomeModuleItem? = nil
var viralHitsItem: SRHomeModuleItem? = nil
var premiereNowItem: SRHomeModuleItem? = nil
list.forEach {
switch $0.module_key {
case .banner:
bannerItem = $0
case .detailsRecommand:
detailsRecommandItem = $0
case .popular:
popularItem = $0
case .updates:
updatesItem = $0
case .bingeWorthy:
bingeWorthyItem = $0
case .viralHits:
viralHitsItem = $0
case .premiereNow:
premiereNowItem = $0
default:
break
}
}
moduleArr.removeAll()
if let item = bannerItem {
moduleArr.append(item)
}
if let item = detailsRecommandItem {
moduleArr.append(item)
}
if let item = popularItem {
moduleArr.append(item)
}
if let item = updatesItem {
moduleArr.append(item)
}
if let item = bingeWorthyItem {
moduleArr.append(item)
}
if let item = viralHitsItem {
moduleArr.append(item)
}
if let item = premiereNowItem {
moduleArr.append(item)
}
}
}

View File

@ -0,0 +1,53 @@
//
// SRSearchViewModel.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRSearchViewModel: NSObject {
static let searchRecordUserDefaultKey = "SRSearchViewModel.searchRecordUserDefaultKey"
@objc dynamic private(set) var recordList: [String] = (UserDefaults.standard.object(forKey: SRSearchViewModel.searchRecordUserDefaultKey) as? [String]) ?? []
@objc dynamic private(set) lazy var hotDataArr: [SRShortModel] = []
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: SRSearchViewModel.searchRecordUserDefaultKey)
}
func clearSearchRecord() {
recordList.removeAll()
UserDefaults.standard.set(recordList, forKey: SRSearchViewModel.searchRecordUserDefaultKey)
}
///
func requestHotSearchData() async {
if let list = await SRHomeApi.requestHotSearchData(), list.count > 0 {
self.hotDataArr = list
}
}
}

View File

@ -0,0 +1,37 @@
//
// SRMyShortViewController.swift
// SynthReel
//
// Created by on 2025/11/20.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SRMyShortViewController: SRViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundImageView.image = UIImage(named: "my_short_bg_image")
self.backgroundImageView.contentMode = .scaleAspectFit
sr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
extension SRMyShortViewController {
private func sr_setupUI() {
self.backgroundImageView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
}
}

View File

@ -0,0 +1,20 @@
//
// SRShortDetailModel.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SmartCodable
class SRShortDetailModel: NSObject, SmartCodable {
required override init() { }
var episodeList: [SRVideoInfoModel]?
var video_info: SRVideoInfoModel?
var shortPlayInfo: SRShortModel?
var is_collect: Bool?
var share_coin: Int?
}

View File

@ -0,0 +1,58 @@
//
// SRShortModel.swift
// SynthReel
//
// Created by on 2025/11/14.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SmartCodable
class SRShortModel: NSObject, SmartCodable {
required override init() { }
var id: String?
var sr_description: String?
var name: String?
var watch_total: Int?
var collect_total: Int?
var current_episode: String?
var short_play_video_id: String?
var image_url: String?
var is_collect: Bool?
var episode_total: Int?
var horizontally_img: String?
var category: [String]?
var video_url: String?
var categoryList: [SRCategoryModel]?
var short_play_id: String?
var video_info: SRVideoInfoModel?
@SmartIgnored
var cellHeight: CGFloat = 0
static func mappingForKey() -> [SmartKeyTransformer]? {
return [
CodingKeys.sr_description <--- ["description", "short_video_description"],
CodingKeys.name <--- ["short_video_title", "name"]
]
}
}
class SRVideoInfoModel: NSObject, SmartCodable {
required override init() { }
var episode: String?
var short_play_id: String?
var coins: Int?
var video_url: String?
///
var is_lock: Bool?
var short_play_video_id: String?
///
var play_seconds: Int?
var image_url: String?
}

View File

@ -0,0 +1,91 @@
//
// SREpSelectorCell.swift
// SynthReel
//
// Created by on 2025/11/19.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SnapKit
class SREpSelectorCell: UICollectionViewCell {
var model: SRVideoInfoModel? {
didSet {
numLabel.text = model?.episode
}
}
var sr_isSelected: Bool = false {
didSet {
if sr_isSelected {
numLabel.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
boderView.isHidden = false
} else {
numLabel.textColors = [UIColor.white.cgColor, UIColor.white.cgColor]
boderView.isHidden = true
}
}
}
lazy var numLabel: SRLabel = {
let label = SRLabel()
label.font = .font(ofSize: 14, weight: .regular)
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
return label
}()
lazy var boderView: SRGradientView = {
let view = SRGradientView()
view.colors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
view.startPoint = .init(x: 0.5, y: 0)
view.endPoint = .init(x: 0.5, y: 1)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
return view
}()
lazy var boderLayer: CAShapeLayer = {
let layer = CAShapeLayer()
return layer
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.layer.cornerRadius = 10
contentView.layer.masksToBounds = true
contentView.backgroundColor = ._1_B_1_B_1_B
boderLayer.fillColor = contentView.backgroundColor?.cgColor //
contentView.addSubview(boderView)
boderView.layer.addSublayer(boderLayer)
contentView.addSubview(numLabel)
numLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
boderView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
let boderWidth: CGFloat = 1
boderLayer.path = UIBezierPath(roundedRect: .init(x: boderWidth, y: boderWidth, width: size.width - boderWidth * 2, height: size.height - boderWidth * 2), cornerRadius: 9.5).cgPath
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,197 @@
//
// SREpSelectorView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import HWPanModal
import SnapKit
class SREpSelectorView: SRPanModalContentView {
var didSelected: ((_ index: Int) -> Void)?
var model: SRShortDetailModel? {
didSet {
coverImageView.sr_setImage(model?.shortPlayInfo?.image_url)
shortNameLabel.text = model?.shortPlayInfo?.name
desLabel.text = model?.shortPlayInfo?.sr_description
subtitleLabel.text = "all_episodes_text".localizedReplace(text: "\(model?.shortPlayInfo?.episode_total ?? 0)")
if let text = model?.shortPlayInfo?.category?.first, text.count > 0 {
cagetoryLabel.text = "#" + text
} else {
cagetoryLabel.text = ""
}
self.collectionView.reloadData()
}
}
var selectedIndex: Int = 0 {
didSet {
self.collectionView.reloadData()
}
}
lazy var coverBgView = UIImageView(image: UIImage(named: "ep_cover_bg_image"))
lazy var coverImageView: UIImageView = {
let imageView = SRImageView()
imageView.layer.cornerRadius = 2
return imageView
}()
lazy var shortNameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 15, weight: .semibold)
label.textColor = .srBlue
return label
}()
lazy var cagetoryLabel: SRLabel = {
let label = SRLabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .A_6_A_6_A_6
label.numberOfLines = 3
return label
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 15, weight: .medium)
label.textColor = .white
label.text = "Select Episode".localized
return label
}()
lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .CCCCCC
return label
}()
lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let itemWidth = (UIScreen.width - 30 - 40) / 5
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15)
layout.itemSize = .init(width: floor(itemWidth), height: 50)
return layout
}()
lazy var collectionView: SRCollectionView = {
let collectionView = SRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.showsVerticalScrollIndicator = false
collectionView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
collectionView.register(SREpSelectorCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension SREpSelectorView {
private func sr_setupUI() {
addSubview(coverBgView)
addSubview(coverImageView)
addSubview(shortNameLabel)
addSubview(cagetoryLabel)
addSubview(desLabel)
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(collectionView)
coverBgView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.top.equalToSuperview().offset(18)
}
coverImageView.snp.makeConstraints { make in
make.center.equalTo(coverBgView)
make.width.equalTo(63)
make.height.equalTo(84)
}
shortNameLabel.snp.makeConstraints { make in
make.left.equalTo(coverImageView.snp.right).offset(10)
make.top.equalToSuperview().offset(24)
make.right.lessThanOrEqualToSuperview().offset(-15)
}
cagetoryLabel.snp.makeConstraints { make in
make.left.equalTo(shortNameLabel)
make.top.equalTo(shortNameLabel.snp.bottom).offset(8)
}
desLabel.snp.makeConstraints { make in
make.left.equalTo(shortNameLabel)
make.right.lessThanOrEqualToSuperview().offset(-15)
make.top.equalTo(shortNameLabel.snp.bottom).offset(32)
}
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.top.equalTo(coverBgView.snp.bottom).offset(16)
}
subtitleLabel.snp.makeConstraints { make in
make.centerY.equalTo(titleLabel)
make.left.equalTo(titleLabel.snp.right).offset(3)
}
collectionView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(166)
make.bottom.equalToSuperview()
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension SREpSelectorView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SREpSelectorCell
cell.model = self.model?.episodeList?[indexPath.row]
cell.sr_isSelected = indexPath.row == self.selectedIndex
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.model?.episodeList?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.didSelected?(indexPath.row)
Task {
await self.dismiss(animated: true)
}
}
}

View File

@ -0,0 +1,216 @@
//
// SRProgressView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import YYText
import YYCategories
class SRProgressView: 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.white.withAlphaComponent(0.2)
var currentProgress = UIColor.white
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: 0, bottom: 0, right: 0) {
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 currentProgressWidth = progressWidth * progress
///
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()
///
let path = UIBezierPath(arcCenter: .init(x: currentProgressWidth + progressX, y: progressY + lineWidth / 2), radius: 3, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
context.addPath(path.cgPath)
context.setFillColor(currentProgress.cgColor)
context.fillPath()
}
}
}
extension SRProgressView {
@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)
}
}

View File

@ -0,0 +1,26 @@
//
// SRRecommendPlayerCell.swift
// SynthReel
//
// Created by on 2025/11/20.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
class SRRecommendPlayerCell: JXPlayerListCell {
override var ControlViewClass: JXPlayerListControlView.Type {
return SRRecommendPlayerControlView.self
}
override var model: Any? {
didSet {
let model = self.model as? SRShortModel
let videoInfo = model?.video_info
self.player.setPlayUrl(url: videoInfo?.video_url ?? "")
self.player.coverImageView?.sr_setImage(model?.image_url)
}
}
}

View File

@ -0,0 +1,226 @@
//
// SRRecommendPlayerControlView.swift
// SynthReel
//
// Created by on 2025/11/20.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
import SnapKit
import YYCategories
class SRRecommendPlayerControlView: JXPlayerListControlView {
override var viewModel: JXPlayerListViewModel? {
didSet {
self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil)
}
}
override var model: Any? {
didSet {
let model = model as! SRShortModel
shortNameLabel.text = model.name
stackView.sr_removeAllArrangedSubview()
if let text = model.category?.first, text.count > 0 {
categoryLabel.text = "#" + text
stackView.addArrangedSubview(categoryLabel)
}
if let text = model.sr_description, text.count > 0 {
desLabel.text = text
stackView.addArrangedSubview(desLabel)
}
}
}
override var isCurrent: Bool {
didSet {
updatePlayerViewStatus()
}
}
lazy var controlerView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "short_progress_bg_image"))
imageView.isUserInteractionEnabled = true
return imageView
}()
lazy var shortNameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .semibold)
label.textColor = .srBlue
return label
}()
lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 8
return view
}()
lazy var categoryLabel: SRLabel = {
let label = SRLabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColors = [UIColor.srGreen.cgColor, UIColor.srBlue.cgColor]
label.textStartPoint = .init(x: 0.5, y: 0)
label.textEndPoint = .init(x: 0.5, y: 1)
return label
}()
lazy var desLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 11, weight: .regular)
label.textColor = .A_6_A_6_A_6
label.numberOfLines = 2
return label
}()
lazy var epBgView: UIView = {
let view = SRGradientView()
view.colors = [UIColor._51_D_4_FF.withAlphaComponent(0.5).cgColor, UIColor._4_CFFD_4.withAlphaComponent(0.1).cgColor]
view.startPoint = .init(x: 0, y: 0.5)
view.endPoint = .init(x: 1, y: 0.5)
view.layer.cornerRadius = 2
view.layer.masksToBounds = true
let tap = UITapGestureRecognizer { [weak self] _ in
guard let self = self else { return }
let vc = SRDetailPlayerViewController()
vc.shortId = (self.model as? SRShortModel)?.short_play_id
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
view.addGestureRecognizer(tap)
return view
}()
lazy var epIconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "ep_icon_02"))
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
return imageView
}()
lazy var epTextLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .white
label.text = "recommend_ep_text".localized
return label
}()
lazy var indicatorImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "arrow_right_icon_02"))
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
return imageView
}()
lazy var playerImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "play_icon_02"))
imageView.isHidden = true
return imageView
}()
deinit {
self.viewModel?.removeObserver(self, forKeyPath: "isPlaying")
}
override init(frame: CGRect) {
super.init(frame: frame)
sr_setupUI()
let tap = UITapGestureRecognizer { [weak self] _ in
guard let self = self else { return }
self.viewModel?.userSwitchPlayAndPause()
}
self.addGestureRecognizer(tap)
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "isPlaying" {
updatePlayerViewStatus()
}
}
func updatePlayerViewStatus() {
if self.viewModel?.isPlaying == true || !isCurrent {
playerImageView.isHidden = true
} else {
playerImageView.isHidden = false
}
}
}
extension SRRecommendPlayerControlView {
private func sr_setupUI() {
addSubview(controlerView)
controlerView.addSubview(shortNameLabel)
controlerView.addSubview(stackView)
controlerView.addSubview(epBgView)
epBgView.addSubview(epIconImageView)
epBgView.addSubview(epTextLabel)
epBgView.addSubview(indicatorImageView)
addSubview(playerImageView)
controlerView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-10)
}
shortNameLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.right.lessThanOrEqualToSuperview().offset(-12)
make.top.equalToSuperview().offset(13)
}
stackView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.right.lessThanOrEqualToSuperview().offset(-12)
make.top.equalTo(shortNameLabel.snp.bottom).offset(8)
make.bottom.equalToSuperview().offset(-52)
}
epBgView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.right.equalToSuperview().offset(-12)
make.bottom.equalToSuperview().offset(-18)
make.height.equalTo(26)
}
epIconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
epTextLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(epIconImageView.snp.right).offset(4)
make.right.lessThanOrEqualTo(self.indicatorImageView.snp.left).offset(-5)
}
indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-12)
}
playerImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
}

View File

@ -0,0 +1,239 @@
//
// SRShortDetailControlView.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
import SnapKit
class SRShortDetailControlView: JXPlayerListControlView {
var sr_viewModel: SRShortPlayerViewModel? {
return self.viewModel as? SRShortPlayerViewModel
}
override var viewModel: JXPlayerListViewModel? {
didSet {
self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil)
}
}
var shortModel: SRShortModel? {
didSet {
titleLabel.text = shortModel?.name
collectButton.isSelected = shortModel?.is_collect == true
}
}
override var durationTime: TimeInterval {
didSet {
updateProgress()
let (_, m, s) = Int(durationTime).formatTimeGroup()
totalTimeLabel.text = "\(m):\(s)"
}
}
override var currentTime: TimeInterval {
didSet {
updateProgress()
let (_, m, s) = Int(currentTime).formatTimeGroup()
currentTimeLabel.text = "\(m):\(s)"
}
}
override var isLoading: Bool {
didSet {
progressView.isLoading = isLoading
}
}
lazy var progressBgView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "short_progress_bg_image"))
imageView.isUserInteractionEnabled = true
return imageView
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .srBlue
return label
}()
lazy var progressView: SRProgressView = {
let view = SRProgressView()
view.insets = .init(top: 10, left: 5, bottom: 10, right: 5)
view.panFinish = { [weak self] progress in
guard let self = self else { return }
self.viewModel?.seekTo(Float(progress))
}
return view
}()
lazy var totalTimeLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .DFDFDF
label.text = "00:00"
return label
}()
lazy var currentTimeLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .regular)
label.textColor = .DFDFDF
label.text = "00:00"
return label
}()
lazy var epButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.sr_viewModel?.onEpSelectorView()
}))
button.setImage(UIImage(named: "ep_icon_01"), for: .normal)
return button
}()
lazy var collectButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
guard let shortId = self.shortModel?.short_play_id else { return }
let videoId = (self.model as? SRVideoInfoModel)?.short_play_video_id
let isCollect = !(self.shortModel?.is_collect ?? false)
Task {
await SRShortApi.requestShortCollect(shortId: shortId, videoId: videoId, isCollect: isCollect)
}
}))
button.setImage(UIImage(named: "collect_icon_01"), for: .normal)
button.setImage(UIImage(named: "collect_icon_01_selected"), for: .selected)
button.setImage(UIImage(named: "collect_icon_01_selected"), for: [.selected, .highlighted])
return button
}()
lazy var playerImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "play_icon_02"))
imageView.isHidden = true
return imageView
}()
deinit {
self.viewModel?.removeObserver(self, forKeyPath: "isPlaying")
NotificationCenter.default.removeObserver(self)
}
override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: SRShortApi.updateShortCollectStateNotification, object: nil)
let tap = UITapGestureRecognizer { [weak self] _ in
guard let self = self else { return }
self.viewModel?.userSwitchPlayAndPause()
}
self.addGestureRecognizer(tap)
sr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func updateShortCollectStateNotification(sender: Notification) {
guard let userInfo = sender.userInfo else { return }
guard let shortId = userInfo["id"] as? String else { return }
guard let state = userInfo["state"] as? Bool else { return }
guard shortId == self.shortModel?.short_play_id else { return }
self.shortModel?.is_collect = state
collectButton.isSelected = state
}
private func updateProgress() {
guard durationTime > 0 else {
progressView.progress = 0
return
}
progressView.progress = currentTime / durationTime
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "isPlaying" {
updatePlayerViewStatus()
}
}
func updatePlayerViewStatus() {
if self.viewModel?.isPlaying == true || !isCurrent {
playerImageView.isHidden = true
} else {
playerImageView.isHidden = false
}
}
}
extension SRShortDetailControlView {
private func sr_setupUI() {
addSubview(progressBgView)
progressBgView.addSubview(titleLabel)
progressBgView.addSubview(progressView)
progressBgView.addSubview(totalTimeLabel)
progressBgView.addSubview(currentTimeLabel)
addSubview(epButton)
addSubview(collectButton)
addSubview(playerImageView)
progressBgView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-(UIScreen.safeBottom + 5))
make.height.equalTo(88)
}
titleLabel.snp.makeConstraints { make in
make.centerY.equalTo(progressBgView.snp.top).offset(23)
make.left.equalToSuperview().offset(9)
make.right.lessThanOrEqualToSuperview().offset(-9)
}
progressView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(4)
make.right.equalToSuperview().offset(-6)
make.bottom.equalToSuperview().offset(-30)
}
totalTimeLabel.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-11)
make.bottom.equalToSuperview().offset(-24)
}
currentTimeLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(9)
make.bottom.equalToSuperview().offset(-24)
}
epButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-15)
make.bottom.equalTo(progressBgView.snp.top).offset(-44)
}
collectButton.snp.makeConstraints { make in
make.centerX.equalTo(epButton)
make.bottom.equalTo(epButton.snp.top).offset(-25)
}
playerImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
}

View File

@ -0,0 +1,43 @@
//
// SRShortDetailPlayerCell.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
class SRShortDetailPlayerCell: JXPlayerListCell {
override var ControlViewClass: JXPlayerListControlView.Type {
return SRShortDetailControlView.self
}
var sr_controlView: SRShortDetailControlView {
return self.controlView as! SRShortDetailControlView
}
var sr_viewModel: SRShortPlayerViewModel? {
return self.viewModel as? SRShortPlayerViewModel
}
override var model: Any? {
didSet {
let model = self.model as? SRVideoInfoModel
self.player.setPlayUrl(url: model?.video_url ?? "")
// self.lockView.isHidden = !(model?.is_lock ?? true)
// lockView.videoInfo = model
}
}
var shortModel: SRShortModel? {
didSet {
self.sr_controlView.shortModel = shortModel
self.player.coverImageView?.sr_setImage(shortModel?.image_url)
}
}
}

View File

@ -0,0 +1,107 @@
//
// SRDetailPlayerViewController.swift
// SynthReel
//
// Created by on 2025/11/17.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
import SnapKit
class SRDetailPlayerViewController: JXPlayerListViewController {
var shortId: String? {
set {
sr_viewModel.shortId = newValue ?? "0"
}
get {
return sr_viewModel.shortId
}
}
override var ViewModelClass: JXPlayerListViewModel.Type {
return SRShortPlayerViewModel.self
}
var sr_viewModel: SRShortPlayerViewModel {
return self.viewModel as! SRShortPlayerViewModel
}
lazy var returnButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.sr_handleNavigationBack()
}))
button.setImage(UIImage(named: "arrow_left_icon_01"), for: .normal)
return button
}()
deinit {
srPrint(message: "销毁")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .black
self.register(SRShortDetailPlayerCell.self, forCellWithReuseIdentifier: "SRShortDetailPlayerCell")
self.delegate = self
self.dataSource = self
sr_setupUI()
Task {
await self.sr_viewModel.requestShortDetail()
}
}
override func play() {
let videoInfo = self.viewModel.currentCell?.model as? SRVideoInfoModel
super.play()
Task {
await SRShortApi.requestCreatePlayHistory(shortId: videoInfo?.short_play_id, videoId: videoInfo?.short_play_video_id)
}
}
}
extension SRDetailPlayerViewController {
private func sr_setupUI() {
view.addSubview(returnButton)
returnButton.snp.makeConstraints { make in
make.height.equalTo(44)
make.width.equalTo(44)
make.left.equalToSuperview().offset(10)
make.top.equalToSuperview().offset(UIScreen.safeTop)
}
}
}
//MARK: JXPlayerListViewControllerDelegate JXPlayerListViewControllerDataSource
extension SRDetailPlayerViewController: JXPlayerListViewControllerDelegate, JXPlayerListViewControllerDataSource {
func jx_playerListViewController(_ viewController: JXPlayerListViewController, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = self.dequeueReusableCell(withReuseIdentifier: "SRShortDetailPlayerCell", for: indexPath) as! SRShortDetailPlayerCell
cell.model = self.sr_viewModel.dataArr[indexPath.section].episodeList?[indexPath.row]
cell.shortModel = self.sr_viewModel.dataArr[indexPath.section].shortPlayInfo
return cell
}
func jx_playerListViewController(_ viewController: JXPlayerListViewController, numberOfItemsInSection section: Int) -> Int {
return self.sr_viewModel.dataArr[section].episodeList?.count ?? 0
}
func jx_numberOfSections(in viewController: JXPlayerListViewController) -> Int {
return self.sr_viewModel.dataArr.count
}
func jx_playerListViewController(_ viewController: JXPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) {
if let view = self.sr_viewModel.popView as? SREpSelectorView {
view.selectedIndex = indexPath.row
}
}
}

View File

@ -0,0 +1,61 @@
//
// SRRecommendPlayerViewController.swift
// SynthReel
//
// Created by on 2025/11/20.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
class SRRecommendPlayerViewController: JXPlayerListViewController {
override var ViewModelClass: JXPlayerListViewModel.Type {
return SRRecommendPlayerViewModel.self
}
override var contentSize: CGSize {
return .init(width: UIScreen.width, height: UIScreen.height - UIScreen.tabBarHeight)
}
var sr_viewModel: SRRecommendPlayerViewModel {
return self.viewModel as! SRRecommendPlayerViewModel
}
override func viewDidLoad() {
super.viewDidLoad()
self.register(SRRecommendPlayerCell.self, forCellWithReuseIdentifier: "SRRecommendPlayerCell")
self.delegate = self
self.dataSource = self
Task {
await self.sr_viewModel.requestRecommendList(page: 1)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
//MARK: JXPlayerListViewControllerDelegate JXPlayerListViewControllerDataSource
extension SRRecommendPlayerViewController: JXPlayerListViewControllerDelegate, JXPlayerListViewControllerDataSource {
func jx_playerListViewController(_ viewController: JXPlayerListViewController, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = self.dequeueReusableCell(withReuseIdentifier: "SRRecommendPlayerCell", for: indexPath) as! SRRecommendPlayerCell
cell.model = self.sr_viewModel.dataArr[indexPath.row]
return cell
}
func jx_playerListViewController(_ viewController: JXPlayerListViewController, numberOfItemsInSection section: Int) -> Int {
return self.sr_viewModel.dataArr.count
}
}

View File

@ -0,0 +1,54 @@
//
// SRRecommendPlayerViewModel.swift
// SynthReel
//
// Created by on 2025/11/20.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
class SRRecommendPlayerViewModel: JXPlayerListViewModel {
lazy var dataArr: [SRShortModel] = []
nonisolated required init() {
super.init()
}
private func addDataArr(dataArr: [SRShortModel]) {
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 requestRecommendList(page: Int) async {
guard let dataArr = await SRHomeApi.requestHomeRecommendData(page: page) else { return }
if page == 1 {
self.playerListVC?.clearData()
self.dataArr = dataArr
self.playerListVC?.reloadData { [weak self] in
self?.playerListVC?.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false)
}
} else {
self.addDataArr(dataArr: dataArr)
}
}
}

View File

@ -0,0 +1,72 @@
//
// SRShortPlayerViewModel.swift
// SynthReel
//
// Created by on 2025/11/18.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import JXPlayer
import HWPanModal
class SRShortPlayerViewModel: JXPlayerListViewModel {
var shortId: String = "0"
var dataArr: [SRShortDetailModel] = []
weak var popView: UIView?
nonisolated required init() {
super.init()
}
func requestShortDetail(indexPath: IndexPath? = nil) async -> Int? {
let (model, code, _) = await SRShortApi.requestShortDetail(shortId)
guard let model = model else { return code }
self.dataArr.removeAll()
self.dataArr.append(model)
self.playerListVC?.reloadData { [weak self] in
guard let self = self else { return }
var targetIndexPath = IndexPath(row: 0, section: 0)
if let indexPath = indexPath, indexPath.row < (model.episodeList?.count ?? 0) {
targetIndexPath = indexPath
} else if let videoInfo = model.video_info {
var row: Int?
model.episodeList?.enumerated().forEach {
if $1.short_play_video_id == videoInfo.short_play_video_id {
row = $0
}
}
if let row = row {
targetIndexPath = .init(row: row, section: 0)
}
}
self.playerListVC?.scrollToItem(indexPath: targetIndexPath, animated: false)
}
return code
}
}
extension SRShortPlayerViewModel {
func onEpSelectorView() {
let view = SREpSelectorView()
view.model = self.dataArr[currentIndexPath.section]
view.selectedIndex = self.currentIndexPath.row
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
}
}

View File

@ -0,0 +1,18 @@
//
// AppDelegate+Config.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
extension AppDelegate {
func setConfig() {
SRToast.config()
}
}

View File

@ -0,0 +1,46 @@
//
// AppDelegate.swift
// SynthReel
//
// Created by on 2025/11/12.
//
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
SRTool.appDelegate = self
SRNetworkReachableManager.manager.startMonitoring()
self.setConfig()
Task {
await SRAccountManager.manager.updateUserInfo()
}
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}

View File

@ -0,0 +1,65 @@
//
// SceneDelegate.swift
// SynthReel
//
// Created by on 2025/11/12.
//
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
SRTool.sceneDelegate = self
guard let windowScene = (scene as? UIWindowScene) else { return }
SRTool.windowScene = windowScene
window = UIWindow(windowScene: windowScene)
window?.rootViewController = SRTabBarController()
window?.makeKeyAndVisible()
NotificationCenter.default.addObserver(self, selector: #selector(abcd), name: NSNotification.Name(rawValue: "abcd"), object: nil)
}
@objc private func abcd() {
window?.rootViewController = SRTabBarController()
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.
}
}

View File

@ -0,0 +1,49 @@
//
// SRAccountManager.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRAccountManager: NSObject {
static let manager = SRAccountManager()
private(set) var token = UserDefaults.sr_object(forKey: kSRAccountTokenDefaultsKey, as: SRAccountToken.self)
private(set) var userInfo = UserDefaults.sr_object(forKey: kSRUserInfoDefaultsKey, as: SRUserInfo.self)
func setAccountToken(_ token: SRAccountToken?) {
self.token = token
UserDefaults.sr_setObject(token, forKey: kSRAccountTokenDefaultsKey)
}
///
func updateUserInfo() async {
// Task {
// completer?()
// }
if let userInfo = await SRUserApi.requestUserInfo() {
self.userInfo = userInfo
UserDefaults.sr_setObject(userInfo, forKey: kSRUserInfoDefaultsKey)
NotificationCenter.default.post(name: SRAccountManager.userInfoUpdateNotification, object: nil)
}
}
}
extension SRAccountManager {
///
@objc static let userInfoUpdateNotification = NSNotification.Name(rawValue: "SRAccountManager.userInfoUpdateNotification")
///
@objc static let loginStatusChangeNotification = NSNotification.Name(rawValue: "SRAccountManager.loginStatusChangeNotification")
}

View File

@ -0,0 +1,42 @@
//
// SRAccountToken.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SmartCodable
class SRAccountToken: NSObject, SmartCodable, NSSecureCoding {
required override init() { }
var auto_login: Int?
var token: String?
var customer_id: String?
static var supportsSecureCoding: Bool {
get {
return true
}
}
func encode(with coder: NSCoder) {
coder.encode(token, forKey: "token")
coder.encode(customer_id, forKey: "customer_id")
coder.encode(auto_login, forKey: "auto_login")
}
required init?(coder: NSCoder) {
super.init()
token = coder.decodeObject(of: NSString.self, forKey: "token") as? String
customer_id = coder.decodeObject(of: NSString.self, forKey: "customer_id") as? String
auto_login = coder.decodeObject(of: NSNumber.self, forKey: "auto_login")?.intValue
}
}

View File

@ -0,0 +1,66 @@
//
// SRUserInfo.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
import SmartCodable
class SRUserInfo: NSObject, SmartCodable, NSSecureCoding {
required override init() { }
var id: String?
var avator: String?
var coin_left_total: Int?
var family_name: String?
var send_coin_left_total: Int?
var vip_end_time: TimeInterval?
var is_vip: Bool?
var customer_id: String?
var is_tourist: Bool?
func getNickName() -> String {
if let name = family_name, !name.isEmpty {
return name
} else {
return "Visitor"
}
}
var totalCoins: Int {
return (coin_left_total ?? 0) + (send_coin_left_total ?? 0)
}
static var supportsSecureCoding: Bool {
return true
}
func encode(with coder: NSCoder) {
coder.encode(id, forKey: "id")
coder.encode(customer_id, forKey: "customer_id")
coder.encode(is_tourist, forKey: "is_tourist")
coder.encode(avator, forKey: "avator")
coder.encode(family_name, forKey: "family_name")
coder.encode(coin_left_total, forKey: "coin_left_total")
coder.encode(send_coin_left_total, forKey: "send_coin_left_total")
coder.encode(is_vip, forKey: "is_vip")
coder.encode(vip_end_time, forKey: "vip_end_time")
}
required init?(coder: NSCoder) {
super.init()
id = coder.decodeObject(of: NSString.self, forKey: "id") as? String
customer_id = coder.decodeObject(of: NSString.self, forKey: "customer_id") as? String
is_tourist = coder.decodeObject(of: NSNumber.self, forKey: "is_tourist")?.boolValue
avator = coder.decodeObject(of: NSString.self, forKey: "avator") as? String
family_name = coder.decodeObject(of: NSString.self, forKey: "family_name") as? String
coin_left_total = coder.decodeObject(of: NSNumber.self, forKey: "coin_left_total")?.intValue
send_coin_left_total = coder.decodeObject(of: NSNumber.self, forKey: "send_coin_left_total")?.intValue
is_vip = coder.decodeObject(of: NSNumber.self, forKey: "is_vip")?.boolValue
vip_end_time = coder.decodeObject(of: NSNumber.self, forKey: "vip_end_time")?.doubleValue
}
}

View File

@ -0,0 +1,27 @@
//
// SRDeviceId.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRDeviceId {
static let shared = SRDeviceId()
private let key = "com.synthreel.deviceid"
// private init() {}
lazy var id: String = {
if let savedID = SRKeychain.shared.read(key: key) {
return savedID
} else {
let newID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
SRKeychain.shared.save(key: key, value: newID)
return newID
}
}()
}

View File

@ -0,0 +1,59 @@
//
// SRKeychain.swift
// SynthReel
//
// Created by on 2025/11/12.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRKeychain {
static let shared = SRKeychain()
func save(key: String, value: String) {
if let data = value.data(using: .utf8) {
//
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key
] as CFDictionary
SecItemDelete(query)
//
let attributes = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecValueData: data,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
] as CFDictionary
SecItemAdd(attributes, nil)
}
}
func read(key: String) -> String? {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecReturnData: kCFBooleanTrue!,
kSecMatchLimit: kSecMatchLimitOne
] as CFDictionary
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query, &dataTypeRef)
if status == errSecSuccess, let data = dataTypeRef as? Data {
return String(data: data, encoding: .utf8)
}
return nil
}
func delete(key: String) {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key
] as CFDictionary
SecItemDelete(query)
}
}

View File

@ -0,0 +1,22 @@
//
// SRHud.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import SVProgressHUD
struct SRHud {
static func show(containerView: UIView? = nil, type: SVProgressHUDMaskType = .clear) {
SVProgressHUD.setContainerView(containerView)
SVProgressHUD.setDefaultMaskType(type)
SVProgressHUD.show()
}
static func dismiss() {
SVProgressHUD.dismiss()
}
}

View File

@ -0,0 +1,24 @@
//
// SRToast.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import Toast
struct SRToast {
static func config() {
CSToastManager.setTapToDismissEnabled(false)
CSToastManager.setDefaultDuration(2)
CSToastManager.setDefaultPosition(CSToastPositionCenter)
}
static func show(text: String?) {
guard let text = text else { return }
SRTool.keyWindow?.makeToast(text)
}
}

View File

@ -0,0 +1,56 @@
//
// SRTool.swift
// SynthReel
//
// Created by on 2025/11/13.
// Copyright © 2025 SR. All rights reserved.
//
import UIKit
class SRTool {
static var appDelegate: AppDelegate?
static var sceneDelegate: SceneDelegate?
static var windowScene: UIWindowScene?
static var keyWindow: UIWindow? {
return windowScene?.keyWindow
}
static var rootViewController: UIViewController? {
return keyWindow?.rootViewController
}
///
static var lanuchViewController: UIViewController? {
let storyboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let vc = storyboard.instantiateInitialViewController()
return vc
}
static var topViewController: UIViewController? {
var resultVC: UIViewController? = self.rootViewController
if let rootNav = resultVC as? UINavigationController {
resultVC = rootNav.topViewController
}
resultVC = self._topViewController(resultVC)
while resultVC?.presentedViewController != nil {
resultVC = self._topViewController(resultVC?.presentedViewController)
}
return resultVC
}
private static func _topViewController(_ vc: UIViewController?) -> UIViewController? {
if vc is UINavigationController {
return _topViewController((vc as? UINavigationController)?.topViewController)
} else if vc is UITabBarController {
return _topViewController((vc as? UITabBarController)?.selectedViewController)
} else {
return vc
}
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x01",
"green" : "0x01",
"red" : "0x01"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x22",
"green" : "0x1B",
"red" : "0x05"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

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