diff --git a/.gitignore b/.gitignore index 3542e5b..4a1b3de 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ xcuserdata/ *.ipa *.dSYM.zip *.dSYM +Podfile.lock ## Playgrounds timeline.xctimeline @@ -32,6 +33,52 @@ playground.xcworkspace .build/ +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +*.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# ---> Objective-C +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..70aad42 --- /dev/null +++ b/Podfile @@ -0,0 +1,43 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '15.0' + +target 'XSeri' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for XSeri + pod 'SnapKit' + pod 'Kingfisher' + pod 'Moya' + pod 'SmartCodable', '5.0.15' + pod 'ESTabBarController-swift' + pod 'JXSegmentedView' + pod 'collection-view-layouts/TagsLayout' + pod 'FSPagerView' + pod 'YYCategories' + pod 'YYText' + pod 'JXPlayer' + pod 'LYEmptyView' + pod 'FDFullscreenPopGesture' + pod 'SVProgressHUD' + pod 'Toast' + pod 'MJRefresh' + pod 'HWPanModal' + pod 'ZLPhotoBrowser' + + post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + # 统一设置部署版本 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + + # 移除可能导致模拟器运行报错的排除架构设置,让 Xcode 自动处理 + config.build_settings.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]') + config.build_settings.delete('EXCLUDED_ARCHITECTURES') + + # 禁用 Bitcode (现代项目通常不需要) + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end + end +end diff --git a/XSeri.xcodeproj/project.pbxproj b/XSeri.xcodeproj/project.pbxproj new file mode 100644 index 0000000..62ad8af --- /dev/null +++ b/XSeri.xcodeproj/project.pbxproj @@ -0,0 +1,1247 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 50DCF656E93DB15465B55F09 /* Pods_XSeri.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 161AA91CBF35A7C2C85D6649 /* Pods_XSeri.framework */; }; + F347D28D2F03709200786648 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2832F03709200786648 /* AppDelegate.swift */; }; + F347D28E2F03709200786648 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D28A2F03709200786648 /* SceneDelegate.swift */; }; + F347D2902F03709200786648 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F347D2842F03709200786648 /* Assets.xcassets */; }; + F347D2922F03709200786648 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F347D2872F03709200786648 /* LaunchScreen.storyboard */; }; + F347D2992F03730E00786648 /* XSTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2982F03730E00786648 /* XSTabBarController.swift */; }; + F347D29B2F03740000786648 /* XSConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D29A2F03740000786648 /* XSConfig.swift */; }; + F347D29E2F03750000786648 /* XSTabBarItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D29D2F03750000786648 /* XSTabBarItemContentView.swift */; }; + F347D29F2F03A6B100786648 /* XSTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D29E2F03A6B100786648 /* XSTool.swift */; }; + F347D2A12F03A84300786648 /* XSScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2A02F03A84300786648 /* XSScreen.swift */; }; + F347D2A32F03A93200786648 /* XSDefine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2A22F03A93200786648 /* XSDefine.swift */; }; + F347D2A52F03AAB800786648 /* XSNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2A42F03AAB800786648 /* XSNavigationController.swift */; }; + F347D2A72F03AAD700786648 /* XSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2A62F03AAD700786648 /* XSViewController.swift */; }; + F347D2AA2F03AD7600786648 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F347D2A82F03AD7600786648 /* Localizable.strings */; }; + F347D2AC2F03ADC800786648 /* String+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2AB2F03ADBE00786648 /* String+XS.swift */; }; + F347D2B02F03AE6700786648 /* XSHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2AF2F03AE6700786648 /* XSHomeViewController.swift */; }; + F347D2B32F03AF7500786648 /* XSHomeSearchButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2B22F03AF7500786648 /* XSHomeSearchButton.swift */; }; + F347D2B52F03B2B500786648 /* UIFont+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2B42F03B2AF00786648 /* UIFont+XS.swift */; }; + F347D2BA2F03BABC00786648 /* XSHomeData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2B82F03BABC00786648 /* XSHomeData.swift */; }; + F347D2BC2F03C19300786648 /* XSHomePopularViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2BB2F03C19300786648 /* XSHomePopularViewController.swift */; }; + F347D2BE2F03C24B00786648 /* XSCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2BD2F03C24B00786648 /* XSCollectionView.swift */; }; + F347D2C02F03C35C00786648 /* XSHomePopularCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2BF2F03C35C00786648 /* XSHomePopularCell.swift */; }; + F347D2C22F03C59B00786648 /* XSWaterfallFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2C12F03C59B00786648 /* XSWaterfallFlowLayout.swift */; }; + F347D2C42F03C76C00786648 /* XSImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2C32F03C76C00786648 /* XSImageView.swift */; }; + F347D2C62F03DAFB00786648 /* XSHomePopularBigCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2C52F03DAFB00786648 /* XSHomePopularBigCell.swift */; }; + F347D2C82F03DBD300786648 /* UIView+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2C72F03DBCB00786648 /* UIView+XS.swift */; }; + F347D2CA2F03DC9200786648 /* CGMutablePath+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2C92F03DC8E00786648 /* CGMutablePath+XS.swift */; }; + F347D2CC2F03E04400786648 /* AppDelegate+Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2CB2F03E03F00786648 /* AppDelegate+Config.swift */; }; + F347D2CE2F04AFC400786648 /* XSHomeChildViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2CD2F04AFC400786648 /* XSHomeChildViewController.swift */; }; + F347D2D02F04B5BB00786648 /* XSHomeNewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2CF2F04B5BB00786648 /* XSHomeNewViewController.swift */; }; + F347D2D22F04B97600786648 /* XSHomeNewBigCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2D12F04B97600786648 /* XSHomeNewBigCell.swift */; }; + F347D2D42F04BF3D00786648 /* XSHomeNewTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2D32F04BF3D00786648 /* XSHomeNewTitleView.swift */; }; + F347D2D62F04C7D500786648 /* XSHomeNewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2D52F04C7D500786648 /* XSHomeNewCell.swift */; }; + F347D2D82F04CB0300786648 /* XSLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2D72F04CB0300786648 /* XSLabel.swift */; }; + F347D2DA2F04D02800786648 /* XSHomeRankingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2D92F04D02800786648 /* XSHomeRankingsViewController.swift */; }; + F347D2DC2F04EE5F00786648 /* XSHomeRankingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2DB2F04EE5F00786648 /* XSHomeRankingsCell.swift */; }; + F347D2DE2F04F54400786648 /* XSHomeCategoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2DD2F04F54400786648 /* XSHomeCategoriesViewController.swift */; }; + F347D2E02F04F7ED00786648 /* XSHomeCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2DF2F04F7ED00786648 /* XSHomeCategoriesCell.swift */; }; + F347D2E22F09F9E300786648 /* XSHomeCategoriesHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2E12F09F9E300786648 /* XSHomeCategoriesHeaderView.swift */; }; + F347D2E42F09FCB300786648 /* XSHomeCategoriesTagsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2E32F09FCB300786648 /* XSHomeCategoriesTagsCell.swift */; }; + F347D2E82F0A03AB00786648 /* XSNetworkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2E72F0A039E00786648 /* XSNetworkModel.swift */; }; + F347D2EA2F0A047A00786648 /* XSNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2E92F0A047400786648 /* XSNetwork.swift */; }; + F347D2EC2F0A060E00786648 /* XSNetworkTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2EB2F0A060900786648 /* XSNetworkTarget.swift */; }; + F347D2EE2F0A06FB00786648 /* XSURLPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2ED2F0A06E700786648 /* XSURLPath.swift */; }; + F347D2F12F0A080000786648 /* XSKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2F02F0A07FF00786648 /* XSKeychain.swift */; }; + F347D2F32F0A083500786648 /* XSDeviceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2F22F0A083100786648 /* XSDeviceId.swift */; }; + F347D2F62F0A0B0B00786648 /* XSLoginToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2F52F0A0B0700786648 /* XSLoginToken.swift */; }; + F347D2F82F0A0B6D00786648 /* XSUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2F72F0A0B6D00786648 /* XSUserInfo.swift */; }; + F347D2FA2F0A0C5600786648 /* XSLoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2F92F0A0C5600786648 /* XSLoginManager.swift */; }; + F347D2FC2F0A0C9000786648 /* UserDefaults+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2FB2F0A0C8B00786648 /* UserDefaults+XS.swift */; }; + F347D2FE2F0A0D0700786648 /* XSUserDefaultsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D2FD2F0A0D0100786648 /* XSUserDefaultsKey.swift */; }; + F347D3012F0A0D8200786648 /* XSUserAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3002F0A0D7E00786648 /* XSUserAPI.swift */; }; + F347D3032F0A10B600786648 /* XSCryptorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3022F0A10B200786648 /* XSCryptorService.swift */; }; + F347D3062F0A12FC00786648 /* XSToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3052F0A12FC00786648 /* XSToast.swift */; }; + F347D3082F0A134500786648 /* XSHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3072F0A134500786648 /* XSHud.swift */; }; + F347D30A2F0A162800786648 /* XSNetworkPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3092F0A162800786648 /* XSNetworkPlugin.swift */; }; + F347D30C2F0A36E200786648 /* XSNetworkMonitorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D30B2F0A36E200786648 /* XSNetworkMonitorManager.swift */; }; + F347D30E2F0A39DE00786648 /* XSHomeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D30D2F0A39D500786648 /* XSHomeAPI.swift */; }; + F347D3102F0A3AB100786648 /* XSCategoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D30F2F0A3AB100786648 /* XSCategoryModel.swift */; }; + F347D3142F0A429C00786648 /* XSShortModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3132F0A429C00786648 /* XSShortModel.swift */; }; + F347D3162F0A430300786648 /* XSVideoInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3152F0A430300786648 /* XSVideoInfoModel.swift */; }; + F347D31A2F0A545000786648 /* XSDiscoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3192F0A545000786648 /* XSDiscoverViewController.swift */; }; + F347D31D2F0A566800786648 /* XSDiscoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D31C2F0A566800786648 /* XSDiscoverViewModel.swift */; }; + F347D3202F0A57A300786648 /* XSDiscoverPlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D31F2F0A57A300786648 /* XSDiscoverPlayerCell.swift */; }; + F347D3222F0A58C300786648 /* XSVideoAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F347D3212F0A58BE00786648 /* XSVideoAPI.swift */; }; + F35547D02F3DA6CA006F28CD /* XSMineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547CF2F3DA6CA006F28CD /* XSMineViewController.swift */; }; + F35547D22F3DA757006F28CD /* XSTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547D12F3DA757006F28CD /* XSTableView.swift */; }; + F35547D42F3DA7A8006F28CD /* XSTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547D32F3DA7A8006F28CD /* XSTableViewCell.swift */; }; + F35547D72F3DA8B5006F28CD /* XSMineCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547D62F3DA8B5006F28CD /* XSMineCell.swift */; }; + F35547DA2F3DAACB006F28CD /* XSMineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547D92F3DAACB006F28CD /* XSMineItem.swift */; }; + F35547DC2F3DB33C006F28CD /* XSMineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547DB2F3DB33C006F28CD /* XSMineHeaderView.swift */; }; + F35547DE2F3DBAF6006F28CD /* XSAboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547DD2F3DBAF6006F28CD /* XSAboutViewController.swift */; }; + F35547E02F3DC030006F28CD /* XSCommonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547DF2F3DC030006F28CD /* XSCommonViewController.swift */; }; + F35547E22F3DCB82006F28CD /* UINavigationBar+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547E12F3DCB82006F28CD /* UINavigationBar+XS.swift */; }; + F35547E42F3DCCDE006F28CD /* XSAboutCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547E32F3DCCDE006F28CD /* XSAboutCell.swift */; }; + F35547E62F3DD1F3006F28CD /* XSAboutHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547E52F3DD1F3006F28CD /* XSAboutHeaderView.swift */; }; + F35547E92F3DDBDD006F28CD /* XSWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547E82F3DDBDD006F28CD /* XSWebView.swift */; }; + F35547EB2F3DDCCE006F28CD /* XSBaseWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547EA2F3DDCCE006F28CD /* XSBaseWebViewController.swift */; }; + F35547EF2F3EF606006F28CD /* XSMyListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547EE2F3EF606006F28CD /* XSMyListViewController.swift */; }; + F35547F22F3EF78A006F28CD /* XSMyListCollectsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547F12F3EF78A006F28CD /* XSMyListCollectsView.swift */; }; + F35547F42F3EFC37006F28CD /* XSMyListCollectsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547F32F3EFC37006F28CD /* XSMyListCollectsCell.swift */; }; + F35547F62F3F0407006F28CD /* XSMyListHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547F52F3F0407006F28CD /* XSMyListHistoryView.swift */; }; + F35547F82F3F0720006F28CD /* XSMyListHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547F72F3F0720006F28CD /* XSMyListHistoryCell.swift */; }; + F35547FA2F4E9F0A006F28CD /* XSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547F92F4E9F0A006F28CD /* XSView.swift */; }; + F35547FC2F4EA069006F28CD /* XSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547FB2F4EA069006F28CD /* XSButton.swift */; }; + F35547FE2F4EC450006F28CD /* XSMyCollectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547FD2F4EC450006F28CD /* XSMyCollectViewController.swift */; }; + F35548002F4EC665006F28CD /* XSMyCollectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35547FF2F4EC665006F28CD /* XSMyCollectCell.swift */; }; + F35548022F4ECD5C006F28CD /* UIScrollView+Refresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548012F4ECD54006F28CD /* UIScrollView+Refresh.swift */; }; + F35548042F4EDF27006F28CD /* UIStackView+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548032F4EDF21006F28CD /* UIStackView+XS.swift */; }; + F35548062F4FD6DA006F28CD /* XSMinePlayHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548052F4FD6DA006F28CD /* XSMinePlayHistoryView.swift */; }; + F35548082F4FD703006F28CD /* XSMineUserInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548072F4FD703006F28CD /* XSMineUserInfoView.swift */; }; + F355480A2F4FE99F006F28CD /* XSMinePlayHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548092F4FE99F006F28CD /* XSMinePlayHistoryCell.swift */; }; + F355480C2F50398B006F28CD /* NSNumber+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F355480B2F503983006F28CD /* NSNumber+XS.swift */; }; + F355480E2F50485E006F28CD /* XSPanModalContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F355480D2F50485E006F28CD /* XSPanModalContentView.swift */; }; + F35548102F504D74006F28CD /* XSEpSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F355480F2F504D74006F28CD /* XSEpSelectorView.swift */; }; + F35548122F5129F2006F28CD /* XSEpSelectorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548112F5129F2006F28CD /* XSEpSelectorCell.swift */; }; + F35548372F52B86C006F28CD /* XSAppWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548362F52B86C006F28CD /* XSAppWebViewController.swift */; }; + F35548392F52B916006F28CD /* Dictionary+XS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548382F52B913006F28CD /* Dictionary+XS.swift */; }; + F355483B2F52BA4A006F28CD /* XSFeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F355483A2F52BA4A006F28CD /* XSFeedbackViewController.swift */; }; + F355483D2F52BC81006F28CD /* XSSettingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F355483C2F52BC78006F28CD /* XSSettingAPI.swift */; }; + F355483F2F52BD07006F28CD /* XSFeedbackCountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F355483E2F52BD07006F28CD /* XSFeedbackCountModel.swift */; }; + F35548412F52BE6D006F28CD /* XSBaseWebViewController+Script.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548402F52BE64006F28CD /* XSBaseWebViewController+Script.swift */; }; + F35548432F52BFB8006F28CD /* XSWebMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35548422F52BFB8006F28CD /* XSWebMessageModel.swift */; }; + F3585C342F148F0800EEC469 /* XSHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C332F148F0800EEC469 /* XSHomeViewModel.swift */; }; + F3585C362F148FE500EEC469 /* XSHomeModuleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C352F148FE500EEC469 /* XSHomeModuleItem.swift */; }; + F3585C382F1497AF00EEC469 /* XSDiscoverControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C372F1497AF00EEC469 /* XSDiscoverControlView.swift */; }; + F3585C3A2F14999700EEC469 /* XSProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C392F14999700EEC469 /* XSProgressView.swift */; }; + F3585C3C2F14BFAB00EEC469 /* XSCustomTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C3B2F14BFAB00EEC469 /* XSCustomTabBar.swift */; }; + F3585C3F2F14C83700EEC469 /* XSPlayerEpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C3E2F14C83700EEC469 /* XSPlayerEpButton.swift */; }; + F3585C422F14E99C00EEC469 /* XSShortDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C412F14E99C00EEC469 /* XSShortDetailViewController.swift */; }; + F3585C452F14EA0A00EEC469 /* XSShortDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C442F14EA0A00EEC469 /* XSShortDetailViewModel.swift */; }; + F3585C472F14EAE700EEC469 /* XSShortDetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C462F14EAE700EEC469 /* XSShortDetailModel.swift */; }; + F3585C492F14EE8D00EEC469 /* XSShortDetailPlayerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C482F14EE8D00EEC469 /* XSShortDetailPlayerCell.swift */; }; + F3585C4B2F14FD1000EEC469 /* XSShortDetailPlayerControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3585C4A2F14FD1000EEC469 /* XSShortDetailPlayerControlView.swift */; }; + F3B312A22F30A7DA0093B180 /* XSSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312A12F30A7DA0093B180 /* XSSearchViewController.swift */; }; + F3B312A42F30AC9B0093B180 /* XSSearchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312A32F30AC9B0093B180 /* XSSearchData.swift */; }; + F3B312AD2F30ACF60093B180 /* XSSearchHotSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312A92F30ACF60093B180 /* XSSearchHotSectionView.swift */; }; + F3B312AE2F30ACF60093B180 /* XSSearchTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312AC2F30ACF60093B180 /* XSSearchTagsView.swift */; }; + F3B312AF2F30ACF60093B180 /* XSSearchTagCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312AB2F30ACF60093B180 /* XSSearchTagCell.swift */; }; + F3B312B02F30ACF60093B180 /* XSSearchRecentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312AA2F30ACF60093B180 /* XSSearchRecentView.swift */; }; + F3B312B12F30ACF60093B180 /* XSSearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312A62F30ACF60093B180 /* XSSearchHeaderView.swift */; }; + F3B312B22F30ACF60093B180 /* XSSearchHotListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312A82F30ACF60093B180 /* XSSearchHotListItemView.swift */; }; + F3B312B32F30ACF60093B180 /* XSSearchGradientButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312A52F30ACF60093B180 /* XSSearchGradientButton.swift */; }; + F3B312B42F30ACF60093B180 /* XSSearchHotListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312A72F30ACF60093B180 /* XSSearchHotListCardView.swift */; }; + F3B312B72F319CBE0093B180 /* XSEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312B62F319CBE0093B180 /* XSEmpty.swift */; }; + F3B312BD2F30B0A10093B180 /* XSSearchSuggestionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312B82F30B0A10093B180 /* XSSearchSuggestionCell.swift */; }; + F3B312BE2F30B0A10093B180 /* XSSearchResultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312B92F30B0A10093B180 /* XSSearchResultCell.swift */; }; + F3B312BF2F30B2000093B180 /* XSSearchHistoryHotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B312B52F30B2000093B180 /* XSSearchHistoryHotView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 161AA91CBF35A7C2C85D6649 /* Pods_XSeri.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_XSeri.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3711C91B5F8043B3213F7116 /* Pods-XSeri.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-XSeri.debug.xcconfig"; path = "Target Support Files/Pods-XSeri/Pods-XSeri.debug.xcconfig"; sourceTree = ""; }; + A9C8E3A3362E04C75537A56F /* Pods-XSeri.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-XSeri.release.xcconfig"; path = "Target Support Files/Pods-XSeri/Pods-XSeri.release.xcconfig"; sourceTree = ""; }; + F347D26B2F03708600786648 /* XSeri.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XSeri.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F347D2832F03709200786648 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + F347D2842F03709200786648 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F347D2852F03709200786648 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F347D2862F03709200786648 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + F347D28A2F03709200786648 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + F347D2982F03730E00786648 /* XSTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSTabBarController.swift; sourceTree = ""; }; + F347D29A2F03740000786648 /* XSConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSConfig.swift; sourceTree = ""; }; + F347D29D2F03750000786648 /* XSTabBarItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSTabBarItemContentView.swift; sourceTree = ""; }; + F347D29E2F03A6B100786648 /* XSTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSTool.swift; sourceTree = ""; }; + F347D2A02F03A84300786648 /* XSScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSScreen.swift; sourceTree = ""; }; + F347D2A22F03A93200786648 /* XSDefine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSDefine.swift; sourceTree = ""; }; + F347D2A42F03AAB800786648 /* XSNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSNavigationController.swift; sourceTree = ""; }; + F347D2A62F03AAD700786648 /* XSViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSViewController.swift; sourceTree = ""; }; + F347D2A92F03AD7600786648 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + F347D2AB2F03ADBE00786648 /* String+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+XS.swift"; sourceTree = ""; }; + F347D2AF2F03AE6700786648 /* XSHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeViewController.swift; sourceTree = ""; }; + F347D2B22F03AF7500786648 /* XSHomeSearchButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeSearchButton.swift; sourceTree = ""; }; + F347D2B42F03B2AF00786648 /* UIFont+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+XS.swift"; sourceTree = ""; }; + F347D2B82F03BABC00786648 /* XSHomeData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeData.swift; sourceTree = ""; }; + F347D2BB2F03C19300786648 /* XSHomePopularViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomePopularViewController.swift; sourceTree = ""; }; + F347D2BD2F03C24B00786648 /* XSCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCollectionView.swift; sourceTree = ""; }; + F347D2BF2F03C35C00786648 /* XSHomePopularCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomePopularCell.swift; sourceTree = ""; }; + F347D2C12F03C59B00786648 /* XSWaterfallFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSWaterfallFlowLayout.swift; sourceTree = ""; }; + F347D2C32F03C76C00786648 /* XSImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSImageView.swift; sourceTree = ""; }; + F347D2C52F03DAFB00786648 /* XSHomePopularBigCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomePopularBigCell.swift; sourceTree = ""; }; + F347D2C72F03DBCB00786648 /* UIView+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+XS.swift"; sourceTree = ""; }; + F347D2C92F03DC8E00786648 /* CGMutablePath+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGMutablePath+XS.swift"; sourceTree = ""; }; + F347D2CB2F03E03F00786648 /* AppDelegate+Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Config.swift"; sourceTree = ""; }; + F347D2CD2F04AFC400786648 /* XSHomeChildViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeChildViewController.swift; sourceTree = ""; }; + F347D2CF2F04B5BB00786648 /* XSHomeNewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeNewViewController.swift; sourceTree = ""; }; + F347D2D12F04B97600786648 /* XSHomeNewBigCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeNewBigCell.swift; sourceTree = ""; }; + F347D2D32F04BF3D00786648 /* XSHomeNewTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeNewTitleView.swift; sourceTree = ""; }; + F347D2D52F04C7D500786648 /* XSHomeNewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeNewCell.swift; sourceTree = ""; }; + F347D2D72F04CB0300786648 /* XSLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSLabel.swift; sourceTree = ""; }; + F347D2D92F04D02800786648 /* XSHomeRankingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeRankingsViewController.swift; sourceTree = ""; }; + F347D2DB2F04EE5F00786648 /* XSHomeRankingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeRankingsCell.swift; sourceTree = ""; }; + F347D2DD2F04F54400786648 /* XSHomeCategoriesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeCategoriesViewController.swift; sourceTree = ""; }; + F347D2DF2F04F7ED00786648 /* XSHomeCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeCategoriesCell.swift; sourceTree = ""; }; + F347D2E12F09F9E300786648 /* XSHomeCategoriesHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeCategoriesHeaderView.swift; sourceTree = ""; }; + F347D2E32F09FCB300786648 /* XSHomeCategoriesTagsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeCategoriesTagsCell.swift; sourceTree = ""; }; + F347D2E72F0A039E00786648 /* XSNetworkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSNetworkModel.swift; sourceTree = ""; }; + F347D2E92F0A047400786648 /* XSNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSNetwork.swift; sourceTree = ""; }; + F347D2EB2F0A060900786648 /* XSNetworkTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSNetworkTarget.swift; sourceTree = ""; }; + F347D2ED2F0A06E700786648 /* XSURLPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSURLPath.swift; sourceTree = ""; }; + F347D2F02F0A07FF00786648 /* XSKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSKeychain.swift; sourceTree = ""; }; + F347D2F22F0A083100786648 /* XSDeviceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSDeviceId.swift; sourceTree = ""; }; + F347D2F52F0A0B0700786648 /* XSLoginToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSLoginToken.swift; sourceTree = ""; }; + F347D2F72F0A0B6D00786648 /* XSUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSUserInfo.swift; sourceTree = ""; }; + F347D2F92F0A0C5600786648 /* XSLoginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSLoginManager.swift; sourceTree = ""; }; + F347D2FB2F0A0C8B00786648 /* UserDefaults+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+XS.swift"; sourceTree = ""; }; + F347D2FD2F0A0D0100786648 /* XSUserDefaultsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSUserDefaultsKey.swift; sourceTree = ""; }; + F347D3002F0A0D7E00786648 /* XSUserAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSUserAPI.swift; sourceTree = ""; }; + F347D3022F0A10B200786648 /* XSCryptorService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCryptorService.swift; sourceTree = ""; }; + F347D3052F0A12FC00786648 /* XSToast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSToast.swift; sourceTree = ""; }; + F347D3072F0A134500786648 /* XSHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHud.swift; sourceTree = ""; }; + F347D3092F0A162800786648 /* XSNetworkPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSNetworkPlugin.swift; sourceTree = ""; }; + F347D30B2F0A36E200786648 /* XSNetworkMonitorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSNetworkMonitorManager.swift; sourceTree = ""; }; + F347D30D2F0A39D500786648 /* XSHomeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeAPI.swift; sourceTree = ""; }; + F347D30F2F0A3AB100786648 /* XSCategoryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCategoryModel.swift; sourceTree = ""; }; + F347D3132F0A429C00786648 /* XSShortModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSShortModel.swift; sourceTree = ""; }; + F347D3152F0A430300786648 /* XSVideoInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSVideoInfoModel.swift; sourceTree = ""; }; + F347D3192F0A545000786648 /* XSDiscoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSDiscoverViewController.swift; sourceTree = ""; }; + F347D31C2F0A566800786648 /* XSDiscoverViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSDiscoverViewModel.swift; sourceTree = ""; }; + F347D31F2F0A57A300786648 /* XSDiscoverPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSDiscoverPlayerCell.swift; sourceTree = ""; }; + F347D3212F0A58BE00786648 /* XSVideoAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSVideoAPI.swift; sourceTree = ""; }; + F35547CF2F3DA6CA006F28CD /* XSMineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMineViewController.swift; sourceTree = ""; }; + F35547D12F3DA757006F28CD /* XSTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSTableView.swift; sourceTree = ""; }; + F35547D32F3DA7A8006F28CD /* XSTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSTableViewCell.swift; sourceTree = ""; }; + F35547D62F3DA8B5006F28CD /* XSMineCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMineCell.swift; sourceTree = ""; }; + F35547D92F3DAACB006F28CD /* XSMineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMineItem.swift; sourceTree = ""; }; + F35547DB2F3DB33C006F28CD /* XSMineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMineHeaderView.swift; sourceTree = ""; }; + F35547DD2F3DBAF6006F28CD /* XSAboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSAboutViewController.swift; sourceTree = ""; }; + F35547DF2F3DC030006F28CD /* XSCommonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCommonViewController.swift; sourceTree = ""; }; + F35547E12F3DCB82006F28CD /* UINavigationBar+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+XS.swift"; sourceTree = ""; }; + F35547E32F3DCCDE006F28CD /* XSAboutCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSAboutCell.swift; sourceTree = ""; }; + F35547E52F3DD1F3006F28CD /* XSAboutHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSAboutHeaderView.swift; sourceTree = ""; }; + F35547E82F3DDBDD006F28CD /* XSWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSWebView.swift; sourceTree = ""; }; + F35547EA2F3DDCCE006F28CD /* XSBaseWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSBaseWebViewController.swift; sourceTree = ""; }; + F35547EE2F3EF606006F28CD /* XSMyListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMyListViewController.swift; sourceTree = ""; }; + F35547F12F3EF78A006F28CD /* XSMyListCollectsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMyListCollectsView.swift; sourceTree = ""; }; + F35547F32F3EFC37006F28CD /* XSMyListCollectsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMyListCollectsCell.swift; sourceTree = ""; }; + F35547F52F3F0407006F28CD /* XSMyListHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMyListHistoryView.swift; sourceTree = ""; }; + F35547F72F3F0720006F28CD /* XSMyListHistoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMyListHistoryCell.swift; sourceTree = ""; }; + F35547F92F4E9F0A006F28CD /* XSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSView.swift; sourceTree = ""; }; + F35547FB2F4EA069006F28CD /* XSButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSButton.swift; sourceTree = ""; }; + F35547FD2F4EC450006F28CD /* XSMyCollectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMyCollectViewController.swift; sourceTree = ""; }; + F35547FF2F4EC665006F28CD /* XSMyCollectCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMyCollectCell.swift; sourceTree = ""; }; + F35548012F4ECD54006F28CD /* UIScrollView+Refresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Refresh.swift"; sourceTree = ""; }; + F35548032F4EDF21006F28CD /* UIStackView+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+XS.swift"; sourceTree = ""; }; + F35548052F4FD6DA006F28CD /* XSMinePlayHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMinePlayHistoryView.swift; sourceTree = ""; }; + F35548072F4FD703006F28CD /* XSMineUserInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMineUserInfoView.swift; sourceTree = ""; }; + F35548092F4FE99F006F28CD /* XSMinePlayHistoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSMinePlayHistoryCell.swift; sourceTree = ""; }; + F355480B2F503983006F28CD /* NSNumber+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNumber+XS.swift"; sourceTree = ""; }; + F355480D2F50485E006F28CD /* XSPanModalContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSPanModalContentView.swift; sourceTree = ""; }; + F355480F2F504D74006F28CD /* XSEpSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSEpSelectorView.swift; sourceTree = ""; }; + F35548112F5129F2006F28CD /* XSEpSelectorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSEpSelectorCell.swift; sourceTree = ""; }; + F35548362F52B86C006F28CD /* XSAppWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSAppWebViewController.swift; sourceTree = ""; }; + F35548382F52B913006F28CD /* Dictionary+XS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+XS.swift"; sourceTree = ""; }; + F355483A2F52BA4A006F28CD /* XSFeedbackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSFeedbackViewController.swift; sourceTree = ""; }; + F355483C2F52BC78006F28CD /* XSSettingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSettingAPI.swift; sourceTree = ""; }; + F355483E2F52BD07006F28CD /* XSFeedbackCountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSFeedbackCountModel.swift; sourceTree = ""; }; + F35548402F52BE64006F28CD /* XSBaseWebViewController+Script.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XSBaseWebViewController+Script.swift"; sourceTree = ""; }; + F35548422F52BFB8006F28CD /* XSWebMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSWebMessageModel.swift; sourceTree = ""; }; + F3585C332F148F0800EEC469 /* XSHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeViewModel.swift; sourceTree = ""; }; + F3585C352F148FE500EEC469 /* XSHomeModuleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeModuleItem.swift; sourceTree = ""; }; + F3585C372F1497AF00EEC469 /* XSDiscoverControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSDiscoverControlView.swift; sourceTree = ""; }; + F3585C392F14999700EEC469 /* XSProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSProgressView.swift; sourceTree = ""; }; + F3585C3B2F14BFAB00EEC469 /* XSCustomTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCustomTabBar.swift; sourceTree = ""; }; + F3585C3E2F14C83700EEC469 /* XSPlayerEpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSPlayerEpButton.swift; sourceTree = ""; }; + F3585C412F14E99C00EEC469 /* XSShortDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSShortDetailViewController.swift; sourceTree = ""; }; + F3585C442F14EA0A00EEC469 /* XSShortDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSShortDetailViewModel.swift; sourceTree = ""; }; + F3585C462F14EAE700EEC469 /* XSShortDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSShortDetailModel.swift; sourceTree = ""; }; + F3585C482F14EE8D00EEC469 /* XSShortDetailPlayerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSShortDetailPlayerCell.swift; sourceTree = ""; }; + F3585C4A2F14FD1000EEC469 /* XSShortDetailPlayerControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSShortDetailPlayerControlView.swift; sourceTree = ""; }; + F3B312A12F30A7DA0093B180 /* XSSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchViewController.swift; sourceTree = ""; }; + F3B312A32F30AC9B0093B180 /* XSSearchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchData.swift; sourceTree = ""; }; + F3B312A52F30ACF60093B180 /* XSSearchGradientButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchGradientButton.swift; sourceTree = ""; }; + F3B312A62F30ACF60093B180 /* XSSearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchHeaderView.swift; sourceTree = ""; }; + F3B312A72F30ACF60093B180 /* XSSearchHotListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchHotListCardView.swift; sourceTree = ""; }; + F3B312A82F30ACF60093B180 /* XSSearchHotListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchHotListItemView.swift; sourceTree = ""; }; + F3B312A92F30ACF60093B180 /* XSSearchHotSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchHotSectionView.swift; sourceTree = ""; }; + F3B312AA2F30ACF60093B180 /* XSSearchRecentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchRecentView.swift; sourceTree = ""; }; + F3B312AB2F30ACF60093B180 /* XSSearchTagCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchTagCell.swift; sourceTree = ""; }; + F3B312AC2F30ACF60093B180 /* XSSearchTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchTagsView.swift; sourceTree = ""; }; + F3B312B52F30B2000093B180 /* XSSearchHistoryHotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchHistoryHotView.swift; sourceTree = ""; }; + F3B312B62F319CBE0093B180 /* XSEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSEmpty.swift; sourceTree = ""; }; + F3B312B82F30B0A10093B180 /* XSSearchSuggestionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchSuggestionCell.swift; sourceTree = ""; }; + F3B312B92F30B0A10093B180 /* XSSearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSearchResultCell.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F347D2682F03708600786648 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50DCF656E93DB15465B55F09 /* Pods_XSeri.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 08156355D4AE6A9A1ED8E99F /* Pods */ = { + isa = PBXGroup; + children = ( + 3711C91B5F8043B3213F7116 /* Pods-XSeri.debug.xcconfig */, + A9C8E3A3362E04C75537A56F /* Pods-XSeri.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + A539659F388504D75759D442 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 161AA91CBF35A7C2C85D6649 /* Pods_XSeri.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F347D2622F03708600786648 = { + isa = PBXGroup; + children = ( + F347D28C2F03709200786648 /* XSeri */, + F347D26C2F03708600786648 /* Products */, + 08156355D4AE6A9A1ED8E99F /* Pods */, + A539659F388504D75759D442 /* Frameworks */, + ); + sourceTree = ""; + }; + F347D26C2F03708600786648 /* Products */ = { + isa = PBXGroup; + children = ( + F347D26B2F03708600786648 /* XSeri.app */, + ); + name = Products; + sourceTree = ""; + }; + F347D28C2F03709200786648 /* XSeri */ = { + isa = PBXGroup; + children = ( + F347D2952F03716F00786648 /* AppScene */, + F347D2962F03718500786648 /* Base */, + F347D2972F03718B00786648 /* Class */, + F347D2942F03711D00786648 /* Source */, + F347D29C2F03A5A100786648 /* Libs */, + ); + path = XSeri; + sourceTree = ""; + }; + F347D2942F03711D00786648 /* Source */ = { + isa = PBXGroup; + children = ( + F347D2842F03709200786648 /* Assets.xcassets */, + F347D2852F03709200786648 /* Info.plist */, + F347D2872F03709200786648 /* LaunchScreen.storyboard */, + F347D2A82F03AD7600786648 /* Localizable.strings */, + ); + path = Source; + sourceTree = ""; + }; + F347D2952F03716F00786648 /* AppScene */ = { + isa = PBXGroup; + children = ( + F347D2832F03709200786648 /* AppDelegate.swift */, + F347D28A2F03709200786648 /* SceneDelegate.swift */, + F347D2CB2F03E03F00786648 /* AppDelegate+Config.swift */, + ); + path = AppScene; + sourceTree = ""; + }; + F347D2962F03718500786648 /* Base */ = { + isa = PBXGroup; + children = ( + F347D29A2F03806400786648 /* Controller */, + F347D29B2F03A4BE00786648 /* View */, + F347D29D2F03A65F00786648 /* Extension */, + F347D29C2F03740000786648 /* Constants */, + F35547E72F3DDBAB006F28CD /* WebView */, + F347D2E52F0A02DF00786648 /* Networking */, + ); + path = Base; + sourceTree = ""; + }; + F347D2972F03718B00786648 /* Class */ = { + isa = PBXGroup; + children = ( + F35547EC2F3EF5E0006F28CD /* MyList */, + F3B312A02F30A6BA0093B180 /* Mine */, + F347D2AD2F03AE3E00786648 /* Home */, + F347D3172F0A53FE00786648 /* Discover */, + F347D3112F0A41FC00786648 /* Player */, + ); + path = Class; + sourceTree = ""; + }; + F347D29A2F03806400786648 /* Controller */ = { + isa = PBXGroup; + children = ( + F347D2982F03730E00786648 /* XSTabBarController.swift */, + F347D2A42F03AAB800786648 /* XSNavigationController.swift */, + F347D2A62F03AAD700786648 /* XSViewController.swift */, + F35547DF2F3DC030006F28CD /* XSCommonViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + F347D29B2F03A4BE00786648 /* View */ = { + isa = PBXGroup; + children = ( + F347D29D2F03750000786648 /* XSTabBarItemContentView.swift */, + F347D2BD2F03C24B00786648 /* XSCollectionView.swift */, + F347D2C32F03C76C00786648 /* XSImageView.swift */, + F347D2D72F04CB0300786648 /* XSLabel.swift */, + F3585C392F14999700EEC469 /* XSProgressView.swift */, + F3585C3B2F14BFAB00EEC469 /* XSCustomTabBar.swift */, + F35547D12F3DA757006F28CD /* XSTableView.swift */, + F35547D32F3DA7A8006F28CD /* XSTableViewCell.swift */, + F35547F92F4E9F0A006F28CD /* XSView.swift */, + F35547FB2F4EA069006F28CD /* XSButton.swift */, + F355480D2F50485E006F28CD /* XSPanModalContentView.swift */, + ); + path = View; + sourceTree = ""; + }; + F347D29C2F03740000786648 /* Constants */ = { + isa = PBXGroup; + children = ( + F347D2A22F03A93200786648 /* XSDefine.swift */, + F347D29A2F03740000786648 /* XSConfig.swift */, + F347D2A02F03A84300786648 /* XSScreen.swift */, + F347D2FD2F0A0D0100786648 /* XSUserDefaultsKey.swift */, + ); + path = Constants; + sourceTree = ""; + }; + F347D29C2F03A5A100786648 /* Libs */ = { + isa = PBXGroup; + children = ( + F3B312B52F319CA50093B180 /* Empty */, + F347D3042F0A12DB00786648 /* HUD */, + F347D29E2F03A6B100786648 /* XSTool.swift */, + F347D2C12F03C59B00786648 /* XSWaterfallFlowLayout.swift */, + F347D2F42F0A0AED00786648 /* Login */, + F347D2EF2F0A07E800786648 /* DeviceId */, + ); + path = Libs; + sourceTree = ""; + }; + F347D29D2F03A65F00786648 /* Extension */ = { + isa = PBXGroup; + children = ( + F35548382F52B913006F28CD /* Dictionary+XS.swift */, + F355480B2F503983006F28CD /* NSNumber+XS.swift */, + F35548032F4EDF21006F28CD /* UIStackView+XS.swift */, + F35548012F4ECD54006F28CD /* UIScrollView+Refresh.swift */, + F347D2C92F03DC8E00786648 /* CGMutablePath+XS.swift */, + F347D2C72F03DBCB00786648 /* UIView+XS.swift */, + F347D2B42F03B2AF00786648 /* UIFont+XS.swift */, + F347D2AB2F03ADBE00786648 /* String+XS.swift */, + F347D2FB2F0A0C8B00786648 /* UserDefaults+XS.swift */, + F35547E12F3DCB82006F28CD /* UINavigationBar+XS.swift */, + ); + path = Extension; + sourceTree = ""; + }; + F347D2AD2F03AE3E00786648 /* Home */ = { + isa = PBXGroup; + children = ( + F347D2AE2F03AE4E00786648 /* Controller */, + F347D2B12F03AF5300786648 /* View */, + F347D2B92F03BABC00786648 /* Model */, + F3585C322F148EDC00EEC469 /* ViewModel */, + ); + path = Home; + sourceTree = ""; + }; + F347D2AE2F03AE4E00786648 /* Controller */ = { + isa = PBXGroup; + children = ( + F347D2AF2F03AE6700786648 /* XSHomeViewController.swift */, + F347D2CD2F04AFC400786648 /* XSHomeChildViewController.swift */, + F347D2BB2F03C19300786648 /* XSHomePopularViewController.swift */, + F347D2CF2F04B5BB00786648 /* XSHomeNewViewController.swift */, + F347D2D92F04D02800786648 /* XSHomeRankingsViewController.swift */, + F347D2DD2F04F54400786648 /* XSHomeCategoriesViewController.swift */, + F3B312A12F30A7DA0093B180 /* XSSearchViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + F347D2B12F03AF5300786648 /* View */ = { + isa = PBXGroup; + children = ( + F347D2B22F03AF7500786648 /* XSHomeSearchButton.swift */, + F347D2BF2F03C35C00786648 /* XSHomePopularCell.swift */, + F347D2C52F03DAFB00786648 /* XSHomePopularBigCell.swift */, + F347D2D12F04B97600786648 /* XSHomeNewBigCell.swift */, + F347D2D52F04C7D500786648 /* XSHomeNewCell.swift */, + F347D2D32F04BF3D00786648 /* XSHomeNewTitleView.swift */, + F347D2DB2F04EE5F00786648 /* XSHomeRankingsCell.swift */, + F347D2DF2F04F7ED00786648 /* XSHomeCategoriesCell.swift */, + F347D2E12F09F9E300786648 /* XSHomeCategoriesHeaderView.swift */, + F347D2E32F09FCB300786648 /* XSHomeCategoriesTagsCell.swift */, + F3B312A52F30ACF60093B180 /* XSSearchGradientButton.swift */, + F3B312A62F30ACF60093B180 /* XSSearchHeaderView.swift */, + F3B312A72F30ACF60093B180 /* XSSearchHotListCardView.swift */, + F3B312A82F30ACF60093B180 /* XSSearchHotListItemView.swift */, + F3B312A92F30ACF60093B180 /* XSSearchHotSectionView.swift */, + F3B312B52F30B2000093B180 /* XSSearchHistoryHotView.swift */, + F3B312AA2F30ACF60093B180 /* XSSearchRecentView.swift */, + F3B312AB2F30ACF60093B180 /* XSSearchTagCell.swift */, + F3B312AC2F30ACF60093B180 /* XSSearchTagsView.swift */, + F3B312B82F30B0A10093B180 /* XSSearchSuggestionCell.swift */, + F3B312B92F30B0A10093B180 /* XSSearchResultCell.swift */, + ); + path = View; + sourceTree = ""; + }; + F347D2B92F03BABC00786648 /* Model */ = { + isa = PBXGroup; + children = ( + F3B312A32F30AC9B0093B180 /* XSSearchData.swift */, + F347D2B82F03BABC00786648 /* XSHomeData.swift */, + F347D30F2F0A3AB100786648 /* XSCategoryModel.swift */, + F3585C352F148FE500EEC469 /* XSHomeModuleItem.swift */, + ); + path = Model; + sourceTree = ""; + }; + F347D2E52F0A02DF00786648 /* Networking */ = { + isa = PBXGroup; + children = ( + F347D2FF2F0A0D7100786648 /* API */, + F347D2E62F0A038D00786648 /* Base */, + ); + path = Networking; + sourceTree = ""; + }; + F347D2E62F0A038D00786648 /* Base */ = { + isa = PBXGroup; + children = ( + F347D2E92F0A047400786648 /* XSNetwork.swift */, + F347D2EB2F0A060900786648 /* XSNetworkTarget.swift */, + F347D2E72F0A039E00786648 /* XSNetworkModel.swift */, + F347D2ED2F0A06E700786648 /* XSURLPath.swift */, + F347D3022F0A10B200786648 /* XSCryptorService.swift */, + F347D3092F0A162800786648 /* XSNetworkPlugin.swift */, + F347D30B2F0A36E200786648 /* XSNetworkMonitorManager.swift */, + ); + path = Base; + sourceTree = ""; + }; + F347D2EF2F0A07E800786648 /* DeviceId */ = { + isa = PBXGroup; + children = ( + F347D2F22F0A083100786648 /* XSDeviceId.swift */, + F347D2F02F0A07FF00786648 /* XSKeychain.swift */, + ); + path = DeviceId; + sourceTree = ""; + }; + F347D2F42F0A0AED00786648 /* Login */ = { + isa = PBXGroup; + children = ( + F347D2F92F0A0C5600786648 /* XSLoginManager.swift */, + F347D2F52F0A0B0700786648 /* XSLoginToken.swift */, + F347D2F72F0A0B6D00786648 /* XSUserInfo.swift */, + ); + path = Login; + sourceTree = ""; + }; + F347D2FF2F0A0D7100786648 /* API */ = { + isa = PBXGroup; + children = ( + F355483C2F52BC78006F28CD /* XSSettingAPI.swift */, + F347D3212F0A58BE00786648 /* XSVideoAPI.swift */, + F347D30D2F0A39D500786648 /* XSHomeAPI.swift */, + F347D3002F0A0D7E00786648 /* XSUserAPI.swift */, + ); + path = API; + sourceTree = ""; + }; + F347D3042F0A12DB00786648 /* HUD */ = { + isa = PBXGroup; + children = ( + F347D3052F0A12FC00786648 /* XSToast.swift */, + F347D3072F0A134500786648 /* XSHud.swift */, + ); + path = HUD; + sourceTree = ""; + }; + F347D3112F0A41FC00786648 /* Player */ = { + isa = PBXGroup; + children = ( + F3585C402F14E96000EEC469 /* Controller */, + F3585C3D2F14C81000EEC469 /* View */, + F347D3122F0A424800786648 /* Model */, + F3585C432F14E9AB00EEC469 /* ViewModel */, + ); + path = Player; + sourceTree = ""; + }; + F347D3122F0A424800786648 /* Model */ = { + isa = PBXGroup; + children = ( + F347D3132F0A429C00786648 /* XSShortModel.swift */, + F347D3152F0A430300786648 /* XSVideoInfoModel.swift */, + F3585C462F14EAE700EEC469 /* XSShortDetailModel.swift */, + ); + path = Model; + sourceTree = ""; + }; + F347D3172F0A53FE00786648 /* Discover */ = { + isa = PBXGroup; + children = ( + F347D3182F0A542400786648 /* Controller */, + F347D31E2F0A577C00786648 /* View */, + F347D31B2F0A562D00786648 /* ViewModel */, + ); + path = Discover; + sourceTree = ""; + }; + F347D3182F0A542400786648 /* Controller */ = { + isa = PBXGroup; + children = ( + F347D3192F0A545000786648 /* XSDiscoverViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + F347D31B2F0A562D00786648 /* ViewModel */ = { + isa = PBXGroup; + children = ( + F347D31C2F0A566800786648 /* XSDiscoverViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + F347D31E2F0A577C00786648 /* View */ = { + isa = PBXGroup; + children = ( + F347D31F2F0A57A300786648 /* XSDiscoverPlayerCell.swift */, + F3585C372F1497AF00EEC469 /* XSDiscoverControlView.swift */, + ); + path = View; + sourceTree = ""; + }; + F35547CE2F3DA68B006F28CD /* Controller */ = { + isa = PBXGroup; + children = ( + F35547CF2F3DA6CA006F28CD /* XSMineViewController.swift */, + F35547DD2F3DBAF6006F28CD /* XSAboutViewController.swift */, + F355483A2F52BA4A006F28CD /* XSFeedbackViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + F35547D52F3DA885006F28CD /* View */ = { + isa = PBXGroup; + children = ( + F35547D62F3DA8B5006F28CD /* XSMineCell.swift */, + F35547DB2F3DB33C006F28CD /* XSMineHeaderView.swift */, + F35547E32F3DCCDE006F28CD /* XSAboutCell.swift */, + F35547E52F3DD1F3006F28CD /* XSAboutHeaderView.swift */, + F35548072F4FD703006F28CD /* XSMineUserInfoView.swift */, + F35548052F4FD6DA006F28CD /* XSMinePlayHistoryView.swift */, + F35548092F4FE99F006F28CD /* XSMinePlayHistoryCell.swift */, + ); + path = View; + sourceTree = ""; + }; + F35547D82F3DAAAF006F28CD /* Model */ = { + isa = PBXGroup; + children = ( + F35547D92F3DAACB006F28CD /* XSMineItem.swift */, + F355483E2F52BD07006F28CD /* XSFeedbackCountModel.swift */, + ); + path = Model; + sourceTree = ""; + }; + F35547E72F3DDBAB006F28CD /* WebView */ = { + isa = PBXGroup; + children = ( + F35547EA2F3DDCCE006F28CD /* XSBaseWebViewController.swift */, + F35548402F52BE64006F28CD /* XSBaseWebViewController+Script.swift */, + F35548362F52B86C006F28CD /* XSAppWebViewController.swift */, + F35547E82F3DDBDD006F28CD /* XSWebView.swift */, + F35548422F52BFB8006F28CD /* XSWebMessageModel.swift */, + ); + path = WebView; + sourceTree = ""; + }; + F35547EC2F3EF5E0006F28CD /* MyList */ = { + isa = PBXGroup; + children = ( + F35547ED2F3EF5E9006F28CD /* Controller */, + F35547F02F3EF740006F28CD /* View */, + ); + path = MyList; + sourceTree = ""; + }; + F35547ED2F3EF5E9006F28CD /* Controller */ = { + isa = PBXGroup; + children = ( + F35547EE2F3EF606006F28CD /* XSMyListViewController.swift */, + F35547FD2F4EC450006F28CD /* XSMyCollectViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + F35547F02F3EF740006F28CD /* View */ = { + isa = PBXGroup; + children = ( + F35547F12F3EF78A006F28CD /* XSMyListCollectsView.swift */, + F35547F32F3EFC37006F28CD /* XSMyListCollectsCell.swift */, + F35547F52F3F0407006F28CD /* XSMyListHistoryView.swift */, + F35547F72F3F0720006F28CD /* XSMyListHistoryCell.swift */, + F35547FF2F4EC665006F28CD /* XSMyCollectCell.swift */, + ); + path = View; + sourceTree = ""; + }; + F3585C322F148EDC00EEC469 /* ViewModel */ = { + isa = PBXGroup; + children = ( + F3585C332F148F0800EEC469 /* XSHomeViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + F3585C3D2F14C81000EEC469 /* View */ = { + isa = PBXGroup; + children = ( + F3585C482F14EE8D00EEC469 /* XSShortDetailPlayerCell.swift */, + F3585C4A2F14FD1000EEC469 /* XSShortDetailPlayerControlView.swift */, + F3585C3E2F14C83700EEC469 /* XSPlayerEpButton.swift */, + F355480F2F504D74006F28CD /* XSEpSelectorView.swift */, + F35548112F5129F2006F28CD /* XSEpSelectorCell.swift */, + ); + path = View; + sourceTree = ""; + }; + F3585C402F14E96000EEC469 /* Controller */ = { + isa = PBXGroup; + children = ( + F3585C412F14E99C00EEC469 /* XSShortDetailViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + F3585C432F14E9AB00EEC469 /* ViewModel */ = { + isa = PBXGroup; + children = ( + F3585C442F14EA0A00EEC469 /* XSShortDetailViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + F3B312A02F30A6BA0093B180 /* Mine */ = { + isa = PBXGroup; + children = ( + F35547CE2F3DA68B006F28CD /* Controller */, + F35547D52F3DA885006F28CD /* View */, + F35547D82F3DAAAF006F28CD /* Model */, + ); + path = Mine; + sourceTree = ""; + }; + F3B312B52F319CA50093B180 /* Empty */ = { + isa = PBXGroup; + children = ( + F3B312B62F319CBE0093B180 /* XSEmpty.swift */, + ); + path = Empty; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F347D26A2F03708600786648 /* XSeri */ = { + isa = PBXNativeTarget; + buildConfigurationList = F347D27E2F03708700786648 /* Build configuration list for PBXNativeTarget "XSeri" */; + buildPhases = ( + EEC7C17BE1E2729F8CB92674 /* [CP] Check Pods Manifest.lock */, + F347D2672F03708600786648 /* Sources */, + F347D2682F03708600786648 /* Frameworks */, + F347D2692F03708600786648 /* Resources */, + 96BE1F08D87500A5F91A3E52 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = XSeri; + productName = XSeri; + productReference = F347D26B2F03708600786648 /* XSeri.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F347D2632F03708600786648 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1640; + LastUpgradeCheck = 1640; + TargetAttributes = { + F347D26A2F03708600786648 = { + CreatedOnToolsVersion = 16.4; + }; + }; + }; + buildConfigurationList = F347D2662F03708600786648 /* Build configuration list for PBXProject "XSeri" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F347D2622F03708600786648; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = F347D26C2F03708600786648 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F347D26A2F03708600786648 /* XSeri */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F347D2692F03708600786648 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F347D2902F03709200786648 /* Assets.xcassets in Resources */, + F347D2AA2F03AD7600786648 /* Localizable.strings in Resources */, + F347D2922F03709200786648 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 96BE1F08D87500A5F91A3E52 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-XSeri/Pods-XSeri-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-XSeri/Pods-XSeri-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-XSeri/Pods-XSeri-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EEC7C17BE1E2729F8CB92674 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-XSeri-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F347D2672F03708600786648 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F347D3162F0A430300786648 /* XSVideoInfoModel.swift in Sources */, + F347D31D2F0A566800786648 /* XSDiscoverViewModel.swift in Sources */, + F347D2F12F0A080000786648 /* XSKeychain.swift in Sources */, + F347D3142F0A429C00786648 /* XSShortModel.swift in Sources */, + F347D2C62F03DAFB00786648 /* XSHomePopularBigCell.swift in Sources */, + F347D3062F0A12FC00786648 /* XSToast.swift in Sources */, + F347D2A52F03AAB800786648 /* XSNavigationController.swift in Sources */, + F35548002F4EC665006F28CD /* XSMyCollectCell.swift in Sources */, + F35548392F52B916006F28CD /* Dictionary+XS.swift in Sources */, + F347D2E42F09FCB300786648 /* XSHomeCategoriesTagsCell.swift in Sources */, + F35547E62F3DD1F3006F28CD /* XSAboutHeaderView.swift in Sources */, + F347D2C42F03C76C00786648 /* XSImageView.swift in Sources */, + F3585C3F2F14C83700EEC469 /* XSPlayerEpButton.swift in Sources */, + F347D2F82F0A0B6D00786648 /* XSUserInfo.swift in Sources */, + F347D2F32F0A083500786648 /* XSDeviceId.swift in Sources */, + F35547E22F3DCB82006F28CD /* UINavigationBar+XS.swift in Sources */, + F347D30A2F0A162800786648 /* XSNetworkPlugin.swift in Sources */, + F347D3082F0A134500786648 /* XSHud.swift in Sources */, + F347D29E2F03750000786648 /* XSTabBarItemContentView.swift in Sources */, + F347D2FE2F0A0D0700786648 /* XSUserDefaultsKey.swift in Sources */, + F347D2EE2F0A06FB00786648 /* XSURLPath.swift in Sources */, + F347D2DC2F04EE5F00786648 /* XSHomeRankingsCell.swift in Sources */, + F35547FC2F4EA069006F28CD /* XSButton.swift in Sources */, + F35547F22F3EF78A006F28CD /* XSMyListCollectsView.swift in Sources */, + F347D2BC2F03C19300786648 /* XSHomePopularViewController.swift in Sources */, + F347D2CE2F04AFC400786648 /* XSHomeChildViewController.swift in Sources */, + F347D2C02F03C35C00786648 /* XSHomePopularCell.swift in Sources */, + F347D2E22F09F9E300786648 /* XSHomeCategoriesHeaderView.swift in Sources */, + F347D30C2F0A36E200786648 /* XSNetworkMonitorManager.swift in Sources */, + F347D2D82F04CB0300786648 /* XSLabel.swift in Sources */, + F35548082F4FD703006F28CD /* XSMineUserInfoView.swift in Sources */, + F347D2E82F0A03AB00786648 /* XSNetworkModel.swift in Sources */, + F35547FE2F4EC450006F28CD /* XSMyCollectViewController.swift in Sources */, + F347D29B2F03740000786648 /* XSConfig.swift in Sources */, + F35547F42F3EFC37006F28CD /* XSMyListCollectsCell.swift in Sources */, + F347D2A72F03AAD700786648 /* XSViewController.swift in Sources */, + F3585C342F148F0800EEC469 /* XSHomeViewModel.swift in Sources */, + F347D2B02F03AE6700786648 /* XSHomeViewController.swift in Sources */, + F3585C3A2F14999700EEC469 /* XSProgressView.swift in Sources */, + F3585C382F1497AF00EEC469 /* XSDiscoverControlView.swift in Sources */, + F347D2D42F04BF3D00786648 /* XSHomeNewTitleView.swift in Sources */, + F347D28D2F03709200786648 /* AppDelegate.swift in Sources */, + F3585C492F14EE8D00EEC469 /* XSShortDetailPlayerCell.swift in Sources */, + F35548412F52BE6D006F28CD /* XSBaseWebViewController+Script.swift in Sources */, + F347D2DA2F04D02800786648 /* XSHomeRankingsViewController.swift in Sources */, + F347D3102F0A3AB100786648 /* XSCategoryModel.swift in Sources */, + F347D28E2F03709200786648 /* SceneDelegate.swift in Sources */, + F355480E2F50485E006F28CD /* XSPanModalContentView.swift in Sources */, + F347D2C82F03DBD300786648 /* UIView+XS.swift in Sources */, + F355483B2F52BA4A006F28CD /* XSFeedbackViewController.swift in Sources */, + F347D2FA2F0A0C5600786648 /* XSLoginManager.swift in Sources */, + F35547DC2F3DB33C006F28CD /* XSMineHeaderView.swift in Sources */, + F347D2B52F03B2B500786648 /* UIFont+XS.swift in Sources */, + F3585C362F148FE500EEC469 /* XSHomeModuleItem.swift in Sources */, + F35547F82F3F0720006F28CD /* XSMyListHistoryCell.swift in Sources */, + F347D2EA2F0A047A00786648 /* XSNetwork.swift in Sources */, + F347D2A32F03A93200786648 /* XSDefine.swift in Sources */, + F3585C3C2F14BFAB00EEC469 /* XSCustomTabBar.swift in Sources */, + F3585C422F14E99C00EEC469 /* XSShortDetailViewController.swift in Sources */, + F347D2DE2F04F54400786648 /* XSHomeCategoriesViewController.swift in Sources */, + F347D3012F0A0D8200786648 /* XSUserAPI.swift in Sources */, + F3585C452F14EA0A00EEC469 /* XSShortDetailViewModel.swift in Sources */, + F35547D02F3DA6CA006F28CD /* XSMineViewController.swift in Sources */, + F347D2D22F04B97600786648 /* XSHomeNewBigCell.swift in Sources */, + F35548102F504D74006F28CD /* XSEpSelectorView.swift in Sources */, + F35548122F5129F2006F28CD /* XSEpSelectorCell.swift in Sources */, + F355480C2F50398B006F28CD /* NSNumber+XS.swift in Sources */, + F347D2E02F04F7ED00786648 /* XSHomeCategoriesCell.swift in Sources */, + F347D3032F0A10B600786648 /* XSCryptorService.swift in Sources */, + F35547D42F3DA7A8006F28CD /* XSTableViewCell.swift in Sources */, + F347D30E2F0A39DE00786648 /* XSHomeAPI.swift in Sources */, + F3B312B72F319CBE0093B180 /* XSEmpty.swift in Sources */, + F347D2F62F0A0B0B00786648 /* XSLoginToken.swift in Sources */, + F347D2CA2F03DC9200786648 /* CGMutablePath+XS.swift in Sources */, + F347D2BE2F03C24B00786648 /* XSCollectionView.swift in Sources */, + F35547DA2F3DAACB006F28CD /* XSMineItem.swift in Sources */, + F347D2D62F04C7D500786648 /* XSHomeNewCell.swift in Sources */, + F347D2CC2F03E04400786648 /* AppDelegate+Config.swift in Sources */, + F347D3202F0A57A300786648 /* XSDiscoverPlayerCell.swift in Sources */, + F35548022F4ECD5C006F28CD /* UIScrollView+Refresh.swift in Sources */, + F347D2EC2F0A060E00786648 /* XSNetworkTarget.swift in Sources */, + F35547E92F3DDBDD006F28CD /* XSWebView.swift in Sources */, + F347D31A2F0A545000786648 /* XSDiscoverViewController.swift in Sources */, + F355483D2F52BC81006F28CD /* XSSettingAPI.swift in Sources */, + F3B312A42F30AC9B0093B180 /* XSSearchData.swift in Sources */, + F347D2FC2F0A0C9000786648 /* UserDefaults+XS.swift in Sources */, + F35547FA2F4E9F0A006F28CD /* XSView.swift in Sources */, + F347D2BA2F03BABC00786648 /* XSHomeData.swift in Sources */, + F3585C4B2F14FD1000EEC469 /* XSShortDetailPlayerControlView.swift in Sources */, + F347D2A12F03A84300786648 /* XSScreen.swift in Sources */, + F35548062F4FD6DA006F28CD /* XSMinePlayHistoryView.swift in Sources */, + F347D2992F03730E00786648 /* XSTabBarController.swift in Sources */, + F35547D22F3DA757006F28CD /* XSTableView.swift in Sources */, + F355480A2F4FE99F006F28CD /* XSMinePlayHistoryCell.swift in Sources */, + F3B312AD2F30ACF60093B180 /* XSSearchHotSectionView.swift in Sources */, + F3B312AE2F30ACF60093B180 /* XSSearchTagsView.swift in Sources */, + F3B312AF2F30ACF60093B180 /* XSSearchTagCell.swift in Sources */, + F3B312B02F30ACF60093B180 /* XSSearchRecentView.swift in Sources */, + F35548042F4EDF27006F28CD /* UIStackView+XS.swift in Sources */, + F3B312B12F30ACF60093B180 /* XSSearchHeaderView.swift in Sources */, + F3B312B22F30ACF60093B180 /* XSSearchHotListItemView.swift in Sources */, + F3B312B32F30ACF60093B180 /* XSSearchGradientButton.swift in Sources */, + F3B312B42F30ACF60093B180 /* XSSearchHotListCardView.swift in Sources */, + F35547F62F3F0407006F28CD /* XSMyListHistoryView.swift in Sources */, + F3B312BF2F30B2000093B180 /* XSSearchHistoryHotView.swift in Sources */, + F35548432F52BFB8006F28CD /* XSWebMessageModel.swift in Sources */, + F3B312BD2F30B0A10093B180 /* XSSearchSuggestionCell.swift in Sources */, + F3B312BE2F30B0A10093B180 /* XSSearchResultCell.swift in Sources */, + F35547E02F3DC030006F28CD /* XSCommonViewController.swift in Sources */, + F355483F2F52BD07006F28CD /* XSFeedbackCountModel.swift in Sources */, + F35547EB2F3DDCCE006F28CD /* XSBaseWebViewController.swift in Sources */, + F35547EF2F3EF606006F28CD /* XSMyListViewController.swift in Sources */, + F347D2D02F04B5BB00786648 /* XSHomeNewViewController.swift in Sources */, + F3B312A22F30A7DA0093B180 /* XSSearchViewController.swift in Sources */, + F347D3222F0A58C300786648 /* XSVideoAPI.swift in Sources */, + F347D29F2F03A6B100786648 /* XSTool.swift in Sources */, + F35548372F52B86C006F28CD /* XSAppWebViewController.swift in Sources */, + F3585C472F14EAE700EEC469 /* XSShortDetailModel.swift in Sources */, + F347D2B32F03AF7500786648 /* XSHomeSearchButton.swift in Sources */, + F35547E42F3DCCDE006F28CD /* XSAboutCell.swift in Sources */, + F347D2C22F03C59B00786648 /* XSWaterfallFlowLayout.swift in Sources */, + F35547DE2F3DBAF6006F28CD /* XSAboutViewController.swift in Sources */, + F35547D72F3DA8B5006F28CD /* XSMineCell.swift in Sources */, + F347D2AC2F03ADC800786648 /* String+XS.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + F347D2872F03709200786648 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F347D2862F03709200786648 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + F347D2A82F03AD7600786648 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + F347D2A92F03AD7600786648 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F347D27F2F03708700786648 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3711C91B5F8043B3213F7116 /* Pods-XSeri.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8NNUR9HPV3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = XSeri/Source/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = XSeri; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_NSCameraUsageDescription = "The APP needs to access your album to provide screenshots for feedback."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "The APP needs to access your album to provide screenshots for feedback."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = aaaaaa.XSeri; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + F347D2802F03708700786648 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A9C8E3A3362E04C75537A56F /* Pods-XSeri.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8NNUR9HPV3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = XSeri/Source/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = XSeri; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_NSCameraUsageDescription = "The APP needs to access your album to provide screenshots for feedback."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "The APP needs to access your album to provide screenshots for feedback."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = aaaaaa.XSeri; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + F347D2812F03708700786648 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 8NNUR9HPV3; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F347D2822F03708700786648 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 8NNUR9HPV3; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F347D2662F03708600786648 /* Build configuration list for PBXProject "XSeri" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F347D2812F03708700786648 /* Debug */, + F347D2822F03708700786648 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F347D27E2F03708700786648 /* Build configuration list for PBXNativeTarget "XSeri" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F347D27F2F03708700786648 /* Debug */, + F347D2802F03708700786648 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F347D2632F03708600786648 /* Project object */; +} diff --git a/XSeri/AppScene/AppDelegate+Config.swift b/XSeri/AppScene/AppDelegate+Config.swift new file mode 100644 index 0000000..db8a1b9 --- /dev/null +++ b/XSeri/AppScene/AppDelegate+Config.swift @@ -0,0 +1,23 @@ +// +// AppDelegate+Config.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// +import UIKit +import MJRefresh + +extension AppDelegate { + + func config() { + UIView.xs_awake() + XSToast.config() + + MJRefreshConfig.default.languageCode = "en" + + let appearance = UINavigationBarAppearance.defaultAppearance() + UINavigationBar.appearance().scrollEdgeAppearance = appearance + UINavigationBar.appearance().standardAppearance = appearance + } + +} diff --git a/XSeri/AppScene/AppDelegate.swift b/XSeri/AppScene/AppDelegate.swift new file mode 100644 index 0000000..f0844ef --- /dev/null +++ b/XSeri/AppScene/AppDelegate.swift @@ -0,0 +1,53 @@ +// +// AppDelegate.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + XSNetworkMonitorManager.manager.startMonitoring() + NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil) + self.config() + + Task { + await XSLoginManager.manager.updateUserInfo() + } + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + +extension AppDelegate { + + @objc private func networkStatusDidChangeNotification() { + guard XSNetworkMonitorManager.manager.isReachable == true else { return } + Task { + await XSLoginManager.manager.updateUserInfo() + } + } + +} diff --git a/XSeri/AppScene/SceneDelegate.swift b/XSeri/AppScene/SceneDelegate.swift new file mode 100644 index 0000000..27b7413 --- /dev/null +++ b/XSeri/AppScene/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + XSTool.windowScene = windowScene + + self.window = UIWindow(windowScene: windowScene) + self.window?.rootViewController = XSTabBarController() + self.window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} diff --git a/XSeri/Base/Constants/XSConfig.swift b/XSeri/Base/Constants/XSConfig.swift new file mode 100644 index 0000000..933928b --- /dev/null +++ b/XSeri/Base/Constants/XSConfig.swift @@ -0,0 +1,28 @@ +import UIKit + +/// 全局样式常量配置 +struct XSConfig { + + struct Color { + /// TabBar 背景色 (#0F0F0F) + static let tabBarBg = UIColor(red: 0.059, green: 0.059, blue: 0.059, alpha: 1.0) + + /// TabBar 选中颜色 (#FFDAA4) + static let tabBarActive = UIColor(red: 1.0, green: 0.855, blue: 0.643, alpha: 1.0) + + /// TabBar 未选中颜色 (白色 40% 透明度) + static let tabBarInactive = UIColor.white.withAlphaComponent(0.4) + + /// TabBar 分割线颜色 (白色 4% 透明度) + static let tabBarSeparator = UIColor.white.withAlphaComponent(0.04) + } + + struct Font { + /// TabBar 选中字体 (Medium 12pt) + static let tabBarActive = UIFont.systemFont(ofSize: 12, weight: .medium) + + /// TabBar 未选中字体 (Regular 11pt) + static let tabBarInactive = UIFont.systemFont(ofSize: 11, weight: .regular) + } + +} diff --git a/XSeri/Base/Constants/XSDefine.swift b/XSeri/Base/Constants/XSDefine.swift new file mode 100644 index 0000000..bda3d63 --- /dev/null +++ b/XSeri/Base/Constants/XSDefine.swift @@ -0,0 +1,47 @@ +// +// XSDefine.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +///当前系统版本号 +let kXSSystemVersion: String = UIDevice.current.systemVersion +let kXSBundleIdentifier: String = (Bundle.main.infoDictionary!["CFBundleIdentifier"] as? String) ?? "0" + +///app版本号 +let kXSVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0" +let kXSBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0" + +let kXSName: String = (Bundle.main.infoDictionary!["CFBundleDisplayName"] as? String) ?? "" +let kXSBundleName: String = (Bundle.main.infoDictionary!["CFBundleName"] as? String) ?? "" + + +#if DEBUG +func xsLog(_ msg: Any?, file: String = #file, function: String = #function, line: Int = #line) { + if let msg = msg { + print("\n\(Date(timeIntervalSinceNow: 8 * 60 * 60)) \(file.components(separatedBy: "/").last ?? "") \(function) \(line): \(msg)") + } +} +#else +func xsLog(_ msg: Any?) { } +#endif + +public func xs_swizzled_instanceMethod(_ prefix: String, oldClass: Swift.AnyClass!, oldSelector: String, newClass: Swift.AnyClass) { + let newSelector = prefix + "_" + oldSelector; + let originalSelector = NSSelectorFromString(oldSelector) + let swizzledSelector = NSSelectorFromString(newSelector) + + let originalMethod = class_getInstanceMethod(oldClass, originalSelector) + let swizzledMethod = class_getInstanceMethod(newClass, swizzledSelector) + + let isAdd = class_addMethod(oldClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!)) + + if isAdd { + class_replaceMethod(newClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!)) + }else { + method_exchangeImplementations(originalMethod!, swizzledMethod!) + } +} diff --git a/XSeri/Base/Constants/XSScreen.swift b/XSeri/Base/Constants/XSScreen.swift new file mode 100644 index 0000000..c02d1d8 --- /dev/null +++ b/XSeri/Base/Constants/XSScreen.swift @@ -0,0 +1,51 @@ +// +// XSScreen.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +struct XSScreen { + static var screen: UIScreen { + return UIScreen.main + } + + static var width: CGFloat { + return UIScreen.main.bounds.width + } + + static var height: CGFloat { + return UIScreen.main.bounds.height + } + + static var safeTop: CGFloat { + return XSTool.keyWindow?.safeAreaInsets.top ?? 20 + } + + static var safeBottom: CGFloat { + return XSTool.keyWindow?.safeAreaInsets.bottom ?? 0 + } + + static var navBarHeight: CGFloat { + return safeTop + 44 + } + + static var tabBarHeight: CGFloat { + return safeBottom + 49 + } + + static var customTabBarHeight: CGFloat { + return safeBottom + 60 + } + + ///屏幕宽比 + static var widthRatio: CGFloat { + return width / 375 + } + + static func getRatioWidth(size: CGFloat) -> CGFloat { + return self.widthRatio * size + } +} diff --git a/XSeri/Base/Constants/XSUserDefaultsKey.swift b/XSeri/Base/Constants/XSUserDefaultsKey.swift new file mode 100644 index 0000000..4e394cf --- /dev/null +++ b/XSeri/Base/Constants/XSUserDefaultsKey.swift @@ -0,0 +1,11 @@ +// +// XSUserDefaultsKey.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +///登录token +let kXSLoginTokenDefaultsKey = "kXSLoginTokenDefaultsKey" +///用户信息 +let kXSUserInfoDefaultsKey = "kXSUserInfoDefaultsKey" diff --git a/XSeri/Base/Controller/XSCommonViewController.swift b/XSeri/Base/Controller/XSCommonViewController.swift new file mode 100644 index 0000000..43e4762 --- /dev/null +++ b/XSeri/Base/Controller/XSCommonViewController.swift @@ -0,0 +1,32 @@ +// +// XSCommonViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import SnapKit + +class XSCommonViewController: XSViewController { + + + private(set) lazy var bgImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "bg_image_01")) + return imageView + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + view.addSubview(bgImageView) + + bgImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + } + + } + + + +} diff --git a/XSeri/Base/Controller/XSNavigationController.swift b/XSeri/Base/Controller/XSNavigationController.swift new file mode 100644 index 0000000..c149f08 --- /dev/null +++ b/XSeri/Base/Controller/XSNavigationController.swift @@ -0,0 +1,47 @@ +// +// XSNavigationController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import FDFullscreenPopGesture + +class XSNavigationController: UINavigationController { + + override func viewDidLoad() { + super.viewDidLoad() + if #available(iOS 26.0, *) { + self.interactiveContentPopGestureRecognizer?.isEnabled = false + } + fd_fullscreenPopGestureRecognizer.isEnabled = true + } + + + override func pushViewController(_ viewController: UIViewController, animated: Bool) { + if children.count > 0 { + viewController.hidesBottomBarWhenPushed = true + } + super.pushViewController(viewController, animated: animated) + } + + override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { + for (index, value) in viewControllers.enumerated() { + if index != 0 { + value.hidesBottomBarWhenPushed = true + } + } + super.setViewControllers(viewControllers, animated: animated) + } + + + override var childForStatusBarStyle: UIViewController? { + return self.topViewController + } + + override var childForStatusBarHidden: UIViewController? { + return self.topViewController + } + +} diff --git a/XSeri/Base/Controller/XSTabBarController.swift b/XSeri/Base/Controller/XSTabBarController.swift new file mode 100644 index 0000000..0d645dc --- /dev/null +++ b/XSeri/Base/Controller/XSTabBarController.swift @@ -0,0 +1,101 @@ +// +// XSTabBarController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import ESTabBarController_swift + +class XSTabBarController: ESTabBarController { + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .black + + setupCustomTabBar() + setupTabBarAppearance() + setupViewControllers() + } + + /// 使用自定义 TabBar,从根源控制高度(系统会根据 sizeThatFits 进行布局) + private func setupCustomTabBar() { + let tabBar = XSCustomTabBar() + tabBar.delegate = self + self.setValue(tabBar, forKey: "tabBar") + } + + /// 配置 TabBar 外观样式 + private func setupTabBarAppearance() { + // 设置背景颜色 + self.tabBar.backgroundColor = XSConfig.Color.tabBarBg + + // 设置顶部分割线颜色 (使用 XSConfig 中的 white 4%) + self.tabBar.shadowImage = UIImage() + self.tabBar.backgroundImage = UIImage() + + // 设置 TabBar 容器样式:顶部 20pt 圆角 + self.tabBar.layer.cornerRadius = 20 + self.tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.tabBar.clipsToBounds = true + + // 添加顶部分割线视图,因为 ESTabBar 可能会覆盖自定义 shadow + let line = UIView() + line.backgroundColor = XSConfig.Color.tabBarSeparator + self.tabBar.addSubview(line) + line.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 0.5) + } + + /// 初始化子控制器 + private func setupViewControllers() { + let homeVC = createNavController( + rootVC: XSHomeViewController(), + title: "Home".localized, + imageName: "tab_home_unsel", + selectedImageName: "tab_home_sel" + ) + + let discoverVC = createNavController( + rootVC: XSDiscoverViewController(), + title: "Discover".localized, + imageName: "tab_discover_unsel", + selectedImageName: "tab_discover_sel" + ) + + let myListVC = createNavController( + rootVC: XSMyListViewController(), + title: "My List".localized, + imageName: "tab_list_unsel", + selectedImageName: "tab_list_sel" + ) + + let portfolioVC = createNavController( + rootVC: XSMineViewController(), + title: "Portfolio".localized, + imageName: "tab_portfolio_unsel", + selectedImageName: "tab_portfolio_sel" + ) + + viewControllers = [homeVC, discoverVC, myListVC, portfolioVC] + } + + /// 创建导航控制器并配置 ESTabBarItem + private func createNavController(rootVC: UIViewController, title: String, imageName: String, selectedImageName: String) -> UINavigationController { + + let nav = XSNavigationController(rootViewController: rootVC) + + let itemContents = XSTabBarItemContentView() + + // 配置图标和标题 + let item = ESTabBarItem( + itemContents, + title: title, + image: UIImage(named: imageName), + selectedImage: UIImage(named: selectedImageName) + ) + + nav.tabBarItem = item + return nav + } +} diff --git a/XSeri/Base/Controller/XSViewController.swift b/XSeri/Base/Controller/XSViewController.swift new file mode 100644 index 0000000..5405a01 --- /dev/null +++ b/XSeri/Base/Controller/XSViewController.swift @@ -0,0 +1,94 @@ +// +// XSViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import JXSegmentedView + +class XSViewController: UIViewController { + + var backImage: UIImage? { + return UIImage(named: "arrow_left_icon_03") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = ._0_F_0_F_0_F + + if let navi = navigationController { + if navi.visibleViewController == self { + if navi.viewControllers.count > 1 { + configNavigationBack(image: backImage) + } + } + } + } + + func handleHeaderRefresh(_ completer: (() -> Void)?) { + completer?() + } + + func handleFooterRefresh(_ completer: (() -> Void)?) { + completer?() + } + +} + +extension UIViewController { + func configNavigationBack(image: UIImage?) { + if let image = image { + let leftBarButtonItem = UIBarButtonItem(image: image, style: .plain ,target: self,action: #selector(handleNavigationBack)) + navigationItem.leftBarButtonItem = leftBarButtonItem + } else { + navigationItem.leftBarButtonItem = nil + } + } + + @objc func handleNavigationBack() { + self.xs_toLastViewController(animated: true) + } + + func xs_toLastViewController(animated: Bool) { + if self.navigationController != nil + { + if self.navigationController?.viewControllers.count == 1 + { + self.dismiss(animated: animated, completion: nil) + } else { + self.navigationController?.popViewController(animated: animated) + } + } + else if self.presentingViewController != nil { + self.dismiss(animated: animated, completion: nil) + } + } +} + + + +//MARK: JXSegmentedListContainerViewListDelegate +extension XSViewController: JXSegmentedListContainerViewListDelegate { + func listView() -> UIView { + return self.view + } +} + +extension UIViewController { + + func xs_setNavigationStyle(backgroundColor: UIColor = .clear, + titleFont: UIFont = .font(ofSize: 18, weight: .bold), + titleColor: UIColor = .white, + isTranslucent: Bool = true + ) { + self.navigationController?.navigationBar.xs_setTranslucent(isTranslucent) + self.navigationController?.navigationBar.xs_setBackgroundColor(backgroundColor) + self.navigationController?.navigationBar.xs_setTitleTextAttributes([ + NSAttributedString.Key.font : titleFont, + NSAttributedString.Key.foregroundColor : titleColor + ]) + } + +} diff --git a/XSeri/Base/Extension/CGMutablePath+XS.swift b/XSeri/Base/Extension/CGMutablePath+XS.swift new file mode 100644 index 0000000..2b1d280 --- /dev/null +++ b/XSeri/Base/Extension/CGMutablePath+XS.swift @@ -0,0 +1,65 @@ +// +// CGMutablePath+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +struct XSRoundedCorner { + var topLeft:CGFloat = 0 + var topRight:CGFloat = 0 + var bottomLeft:CGFloat = 0 + var bottomRight:CGFloat = 0 + + public static let zero = XSRoundedCorner(topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0) + public init(topLeft: CGFloat, topRight:CGFloat, bottomLeft:CGFloat, bottomRight:CGFloat) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } + static func ==(v1:XSRoundedCorner, v2:XSRoundedCorner) -> Bool { + return v1.bottomLeft == v2.bottomLeft + && v1.bottomRight == v2.bottomRight + && v1.topLeft == v2.topLeft + && v1.topRight == v2.topRight + } + static func !=(v1:XSRoundedCorner, v2:XSRoundedCorner) -> Bool { + return !(v1 == v2) + } +} + +extension CGMutablePath { + func addRadiusRectangle(_ circulars: XSRoundedCorner, rect: CGRect) { + let minX = rect.minX + let minY = rect.minY + let maxX = rect.maxX + let maxY = rect.maxY + + //获取四个圆心 + let topLeftCenterX = minX + circulars.topLeft + let topLeftCenterY = minY + circulars.topLeft + + let topRightCenterX = maxX - circulars.topRight + let topRightCenterY = minY + circulars.topRight + + let bottomLeftCenterX = minX + circulars.bottomLeft + let bottomLeftCenterY = maxY - circulars.bottomLeft + + let bottomRightCenterX = maxX - circulars.bottomRight + let bottomRightCenterY = maxY - circulars.bottomRight + + //顶 左 + addArc(center: CGPoint(x: topLeftCenterX, y: topLeftCenterY), radius: circulars.topLeft, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 3 / 2, clockwise: false) + //顶右 + addArc(center: CGPoint(x: topRightCenterX, y: topRightCenterY), radius: circulars.topRight, startAngle: CGFloat.pi * 3 / 2, endAngle: 0, clockwise: false) + //底右 + addArc(center: CGPoint(x: bottomRightCenterX, y: bottomRightCenterY), radius: circulars.bottomRight, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: false) + //底左 + addArc(center: CGPoint(x: bottomLeftCenterX, y: bottomLeftCenterY), radius: circulars.bottomLeft, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: false) + closeSubpath(); + + } +} diff --git a/XSeri/Base/Extension/Dictionary+XS.swift b/XSeri/Base/Extension/Dictionary+XS.swift new file mode 100644 index 0000000..a8628d6 --- /dev/null +++ b/XSeri/Base/Extension/Dictionary+XS.swift @@ -0,0 +1,23 @@ +// +// Dictionary+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/28. +// + +import UIKit + +extension Dictionary { + + func toJsonString() -> String? { + do { + let data = try JSONSerialization.data(withJSONObject: self) + let jsonStr = String(data: data, encoding: .utf8) + return jsonStr + } catch { + + } + return nil + } + +} diff --git a/XSeri/Base/Extension/NSNumber+XS.swift b/XSeri/Base/Extension/NSNumber+XS.swift new file mode 100644 index 0000000..f73580e --- /dev/null +++ b/XSeri/Base/Extension/NSNumber+XS.swift @@ -0,0 +1,35 @@ +// +// NSNumber+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/26. +// + +import UIKit + +extension NSNumber { + + func toString(maximumFractionDigits: Int = 10, minimumFractionDigits: Int? = nil, roundingMode: NumberFormatter.RoundingMode? = nil) -> String { + let formatter = NumberFormatter() + formatter.minimumIntegerDigits = 1 + formatter.maximumFractionDigits = maximumFractionDigits + if let minimumFractionDigits = minimumFractionDigits { + formatter.minimumFractionDigits = minimumFractionDigits + } + if let roundingMode = roundingMode { + formatter.roundingMode = roundingMode + } + formatter.numberStyle = .none + return formatter.string(from: self) ?? "0" + } + + func format() -> String { + let num = self.floatValue + + if num > 1000 { + return NSNumber(value: num / 1000).toString(maximumFractionDigits: 1) + "K" + } else { + return self.stringValue + } + } +} diff --git a/XSeri/Base/Extension/String+XS.swift b/XSeri/Base/Extension/String+XS.swift new file mode 100644 index 0000000..1eded32 --- /dev/null +++ b/XSeri/Base/Extension/String+XS.swift @@ -0,0 +1,30 @@ +// +// String+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import SmartCodable + +extension String: SmartCodable { + + var localized: String { + var text = NSLocalizedString(self, comment: "") + text = text.replacingOccurrences(of: "
", with: "\n") + return text + } + + func localizedReplace(text: String?) -> String { + return self.localized.replacingOccurrences(of: "##", with: text ?? "") + } +} + +extension String { + var length:Int { return (self as NSString).length } + + func size(_ font: UIFont, _ size: CGSize) -> CGSize { + return (self as NSString).size(for: font, size: size, mode: .byWordWrapping) + } +} diff --git a/XSeri/Base/Extension/UIFont+XS.swift b/XSeri/Base/Extension/UIFont+XS.swift new file mode 100644 index 0000000..5502238 --- /dev/null +++ b/XSeri/Base/Extension/UIFont+XS.swift @@ -0,0 +1,16 @@ +// +// UIFont+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +extension UIFont { + + static func font(ofSize fontSize: CGFloat, weight: Weight) -> UIFont { + return UIFont.systemFont(ofSize: fontSize, weight: weight) + } + +} diff --git a/XSeri/Base/Extension/UINavigationBar+XS.swift b/XSeri/Base/Extension/UINavigationBar+XS.swift new file mode 100644 index 0000000..7a43ccd --- /dev/null +++ b/XSeri/Base/Extension/UINavigationBar+XS.swift @@ -0,0 +1,49 @@ +// +// UINavigationBar+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit + + +extension UINavigationBarAppearance { + static func defaultAppearance() -> UINavigationBarAppearance { + let navBarAppearance = UINavigationBarAppearance() + navBarAppearance.configureWithOpaqueBackground() + navBarAppearance.backgroundColor = .clear + navBarAppearance.backgroundEffect = nil + navBarAppearance.shadowColor = UIColor.clear + navBarAppearance.titleTextAttributes = [ + NSAttributedString.Key.font : UIFont.font(ofSize: 18, weight: .bold), + NSAttributedString.Key.foregroundColor : UIColor.white + ] + return navBarAppearance + } +} + +extension UINavigationBar { + + func xs_setTranslucent(_ isTranslucent: Bool) { + self.isTranslucent = isTranslucent + } + + func xs_setBackgroundColor(_ backgroundColor: UIColor?) { + let appearance = self.standardAppearance + appearance.backgroundColor = backgroundColor + self.standardAppearance = appearance + self.scrollEdgeAppearance = appearance + } + + func xs_setTitleTextAttributes(_ titleTextAttributes: [NSAttributedString.Key : Any]?) { + let appearance = self.standardAppearance + + if let titleTextAttributes = titleTextAttributes { + appearance.titleTextAttributes = titleTextAttributes + } + self.scrollEdgeAppearance = appearance + self.standardAppearance = appearance + + } +} diff --git a/XSeri/Base/Extension/UIScrollView+Refresh.swift b/XSeri/Base/Extension/UIScrollView+Refresh.swift new file mode 100644 index 0000000..ffdecc4 --- /dev/null +++ b/XSeri/Base/Extension/UIScrollView+Refresh.swift @@ -0,0 +1,34 @@ +// +// UIScrollView+Refresh.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/25. +// + +import MJRefresh + +extension UIScrollView { + + func xs_addRefreshHeader(insetTop: CGFloat = 0, block: (() -> Void)?) { + + self.mj_header = MJRefreshNormalHeader(refreshingBlock: { + block?() + }) + self.mj_header?.ignoredScrollViewContentInsetTop = insetTop + } + + func xs_addRefreshFooter(insetBottom: CGFloat = 0, block: (() -> Void)?) { + self.mj_footer = MJRefreshBackNormalFooter(refreshingBlock: { + block?() + }) + self.mj_footer?.ignoredScrollViewContentInsetBottom = insetBottom + } + + func xs_endHeaderRefreshing() { + self.mj_header?.endRefreshing() + } + + func xs_endFooterRefreshing() { + self.mj_footer?.endRefreshing() + } +} diff --git a/XSeri/Base/Extension/UIStackView+XS.swift b/XSeri/Base/Extension/UIStackView+XS.swift new file mode 100644 index 0000000..b51bca9 --- /dev/null +++ b/XSeri/Base/Extension/UIStackView+XS.swift @@ -0,0 +1,22 @@ +// +// UIStackView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/25. +// + + +import UIKit + +extension UIStackView { + + func xs_removeAllArrangedSubview() { + let arrangedSubviews = self.arrangedSubviews + + arrangedSubviews.forEach { + self.removeArrangedSubview($0) + $0.removeFromSuperview() + } + } + +} diff --git a/XSeri/Base/Extension/UIView+XS.swift b/XSeri/Base/Extension/UIView+XS.swift new file mode 100644 index 0000000..3d72ac9 --- /dev/null +++ b/XSeri/Base/Extension/UIView+XS.swift @@ -0,0 +1,65 @@ +// +// UIView+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +extension UIView { + + fileprivate struct AssociatedKeys { + static var xs_roundedCorner: Int? + static var xs_effect: Int? + } + + @objc public static func xs_awake() { + xs_swizzled_instanceMethod("xs", oldClass: self, oldSelector: "layoutSubviews", newClass: self) + } + + @objc func xs_layoutSubviews() { + xs_layoutSubviews() + + xs_updateRoundedCorner() +// +// if let effectView = effectView, effectView.frame != self.bounds { +// effectView.frame = self.bounds +// } + } + +} + +//MARK: -------------- 圆角 -------------- +extension UIView { + + + private var roundedCorner: XSRoundedCorner? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.xs_roundedCorner) as? XSRoundedCorner + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.xs_roundedCorner, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + ///设置圆角 + func xs_setCornerRadius(topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat) { + //清空其它设置方法 + self.roundedCorner = XSRoundedCorner(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight) + xs_updateRoundedCorner() + } + + func xs_updateRoundedCorner() { + guard let roundedCorner = self.roundedCorner else { return } + let rect = self.bounds + + let path = CGMutablePath() + path.addRadiusRectangle(roundedCorner, rect: rect) + + let maskLayer = CAShapeLayer() + maskLayer.frame = self.bounds + maskLayer.path = path + self.layer.mask = maskLayer + } + +} diff --git a/XSeri/Base/Extension/UserDefaults+XS.swift b/XSeri/Base/Extension/UserDefaults+XS.swift new file mode 100644 index 0000000..8978314 --- /dev/null +++ b/XSeri/Base/Extension/UserDefaults+XS.swift @@ -0,0 +1,44 @@ +// +// UserDefaults+XS.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit + + +extension UserDefaults { + + static func xs_setObject(_ obj: NSSecureCoding?, forKey key: String) { + let defaults = UserDefaults.standard + + guard let obj = obj else { + defaults.removeObject(forKey: key) + return + } + + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: obj, requiringSecureCoding: true) + defaults.set(data, forKey: key) + } catch { + print("\(error)") + } + } + + static func xs_object(forKey key: String, as type: T.Type) -> T? { + let defaults = UserDefaults.standard + + guard let data = defaults.data(forKey: key) else { + return nil + } + + do { + let object = try NSKeyedUnarchiver.unarchivedObject(ofClass: type, from: data) + return object + } catch { + print("\(error)") + return nil + } + } +} diff --git a/XSeri/Base/Networking/API/XSHomeAPI.swift b/XSeri/Base/Networking/API/XSHomeAPI.swift new file mode 100644 index 0000000..126e454 --- /dev/null +++ b/XSeri/Base/Networking/API/XSHomeAPI.swift @@ -0,0 +1,105 @@ +// +// XSHomeAPI.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit + +struct XSHomeAPI { + + static func requestHomeData() async -> [XSHomeModuleItem]? { + var parameters = XSNetwork.Parameters(path: "/home/all-modules") + parameters.method = .get + parameters.isToast = true + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + + } + + static func requestHomeNew(page: Int) async -> [XSShortModel]? { + var parameters = XSNetwork.Parameters(path: "/newShortPlayList") + parameters.method = .get + parameters.parameters = [ + "current_page" : page, + "page_size" : 20 + ] + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + } + + static func requestHomeRankings() async -> [XSShortModel]? { + var parameters = XSNetwork.Parameters(path: "/nDayMaxRechargeShortPlayRank") + parameters.method = .get + parameters.parameters = [ + "day" : 30, + ] + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + } + + static func requestCategoryList() async -> [XSCategoryModel]? { + var parameters = XSNetwork.Parameters(path: "/getCategories") + parameters.method = .get + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + } + + static func requestCategoryVideo(id: String, page: Int) async -> [XSShortModel]? { + var parameters = XSNetwork.Parameters(path: "/videoList") + parameters.method = .get + parameters.parameters = [ + "category_id" : id, + "current_page" : page, + "page_size" : 20 + ] + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + } + + static func requestDiscoverData(page: Int) async -> [XSShortModel]? { + + var parameters = XSNetwork.Parameters(path: "/getRecommands") + parameters.method = .get + parameters.parameters = [ + "current_page" : page, + "page_size" : 20 + ] + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + + } + + ///搜索 + static func requestSearch(text: String) async -> [XSShortModel]? { + var parameters = XSNetwork.Parameters(path: "/search") + parameters.method = .get + parameters.parameters = [ + "search" : text + ] + + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + } + + static func requestHotSearchList() async -> [XSShortModel]? { + var parameters = XSNetwork.Parameters(path: "/search/hots") + parameters.method = .get + parameters.isLoding = false + parameters.isToast = false + + let response: XSNetwork.Response> = await XSNetwork.request(parameters: parameters) + return response.data?.list + } + + static func requestTopSearchList() async -> [XSShortModel]? { + var parameters = XSNetwork.Parameters(path: "/getVisitTop") + parameters.method = .get + parameters.isLoding = false + parameters.isToast = false + + let response: XSNetwork.Response<[XSShortModel]> = await XSNetwork.request(parameters: parameters) + return response.data + } +} diff --git a/XSeri/Base/Networking/API/XSSettingAPI.swift b/XSeri/Base/Networking/API/XSSettingAPI.swift new file mode 100644 index 0000000..0f4168e --- /dev/null +++ b/XSeri/Base/Networking/API/XSSettingAPI.swift @@ -0,0 +1,20 @@ +// +// XSSettingAPI.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/28. +// + + +struct XSSettingAPI { + + static func requestFeedbackRedCount() async -> XSFeedbackCountModel? { + + var param = XSNetwork.Parameters(path: "/noticeNum") + param.method = .post + param.isLoding = false + param.isToast = false + let response: XSNetwork.Response = await XSNetwork.request(parameters: param) + return response.data + } +} diff --git a/XSeri/Base/Networking/API/XSUserAPI.swift b/XSeri/Base/Networking/API/XSUserAPI.swift new file mode 100644 index 0000000..6e60572 --- /dev/null +++ b/XSeri/Base/Networking/API/XSUserAPI.swift @@ -0,0 +1,20 @@ +// +// XSUserAPI.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// +import UIKit + + +struct XSUserAPI { + + static func requestUserInfo() async -> XSUserInfo? { + + var parameters = XSNetwork.Parameters(path: "/customer/info") + parameters.method = .get + + let response: XSNetwork.Response = await XSNetwork.request(parameters: parameters) + return response.data + } +} diff --git a/XSeri/Base/Networking/API/XSVideoAPI.swift b/XSeri/Base/Networking/API/XSVideoAPI.swift new file mode 100644 index 0000000..e5fc9f1 --- /dev/null +++ b/XSeri/Base/Networking/API/XSVideoAPI.swift @@ -0,0 +1,116 @@ +// +// XSVideoAPI.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit + + +struct XSVideoAPI { + + static func requestShortDetail(shortPlayId: String) async -> (XSShortDetailModel?, Int?, String?) { + let parameters: [String : Any] = [ + "short_play_id" : shortPlayId, + "video_id" : "0" + ] + var param = XSNetwork.Parameters(path: "/getVideoDetails") + param.method = .get + param.isToast = true + param.parameters = parameters + let response: XSNetwork.Response = await XSNetwork.request(parameters: param) + if response.isSuccess { + return (response.data, response.code, response.msg) + } else { + return (nil, response.code, response.msg) + } + } + + static func requestAddPlayHistory(shortPlayId: String?, videoId: String?) async { + let parameters: [String : Any] = [ + "short_play_id" : shortPlayId ?? "0", + "video_id" : videoId ?? "0" + ] + var param = XSNetwork.Parameters(path: "/createHistory") + param.method = .post + param.isToast = false + param.isLoding = false + param.parameters = parameters + let _: XSNetwork.Response = await XSNetwork.request(parameters: param) + } + + static func requestPlayHistorys(page: Int, pageSize: Int = 20) async -> [XSShortModel]? { + let parameters: [String : Any] = [ + "current_page" : page, + "page_size" : pageSize + ] + var param = XSNetwork.Parameters(path: "/myHistorys") + param.method = .get + param.parameters = parameters + let response: XSNetwork.Response> = await XSNetwork.request(parameters: param) + if response.isSuccess { + return response.data?.list + } else { + return nil + } + } + + static func requestCollectShort(isCollect: Bool, shortId: String, videoId: String?) async -> Bool { + let path: String + if isCollect { + path = "/collect" + } else { + path = "/cancelCollect" + } + + var parameters: [String : Any] = [ + "short_play_id" : shortId, + ] + + if let videoId = videoId { + parameters["video_id"] = videoId + } + + var param = XSNetwork.Parameters(path: path) + param.method = .post + param.isLoding = true + param.parameters = parameters + let response: XSNetwork.Response = await XSNetwork.request(parameters: param) + if response.isSuccess { + await MainActor.run { + NotificationCenter.default.post(name: XSVideoAPI.updateShortCollectStateNotification, object: nil, userInfo: [ + "state" : isCollect, + "id" : shortId, + ]) + } + return true + } else { + return false + } + } + + static func requestCollectList(page: Int, pageSize: Int = 20) async -> [XSShortModel]? { + let parameters: [String : Any] = [ + "current_page" : page, + "page_size" : pageSize + ] + var param = XSNetwork.Parameters(path: "/myCollections") + param.method = .get + param.parameters = parameters + let response: XSNetwork.Response> = await XSNetwork.request(parameters: param) + if response.isSuccess { + return response.data?.list + } else { + return nil + } + } +} + +extension XSVideoAPI { + + ///更新短剧关注状态 [ "state" : isFavorite, "id" : shortPlayId,] + static let updateShortCollectStateNotification = Notification.Name(rawValue: "XSVideoAPI.updateShortCollectStateNotification") + + +} diff --git a/XSeri/Base/Networking/Base/XSCryptorService.swift b/XSeri/Base/Networking/Base/XSCryptorService.swift new file mode 100644 index 0000000..7222249 --- /dev/null +++ b/XSeri/Base/Networking/Base/XSCryptorService.swift @@ -0,0 +1,102 @@ +// +// XSCryptorService.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit + +struct XSCryptorService { + + // 定义常量 + static let EN_STR_TAG: String = "$" // 替换为实际的加密标记 + + // 解密字符串 + static func decrypt(data: String) -> String { + guard data.hasPrefix(EN_STR_TAG) else { +// fatalError("Invalid encoded string") + return data + } + + let decryptedData = deStrBytes(data: data) + return String(data: decryptedData, encoding: .utf8) ?? "" + } + + // 从十六进制字符串解密字节 + static func deStrBytes(data: String) -> Data { + + let hexData = String(data.dropFirst()) + var bytes = Data() + + var index = hexData.startIndex + while index < hexData.endIndex { + let nextIndex = hexData.index(index, offsetBy: 2, limitedBy: hexData.endIndex) ?? hexData.endIndex + let byteString = String(hexData[index.. 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 { + 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 + } + } +} diff --git a/XSeri/Base/Networking/Base/XSNetwork.swift b/XSeri/Base/Networking/Base/XSNetwork.swift new file mode 100644 index 0000000..66c4f81 --- /dev/null +++ b/XSeri/Base/Networking/Base/XSNetwork.swift @@ -0,0 +1,186 @@ +// +// XSNetwork.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import Moya +import SmartCodable + +let kXSRegisterPath = "/customer/register" + +/// 网络请求核心类 +struct XSNetwork { + + /// Moya 提供者,注入 HUD 插件实现解耦 + static let provider = MoyaProvider(plugins: [XSNetworkHUDPlugin()]) + + /// 用于存储正在进行的 Token 刷新任务,防止并发竞争 + /// 使用 _Concurrency.Task 明确指定系统并发任务,避免与 Moya.Task 冲突 + private static var refreshTokenTask: _Concurrency.Task? + + // MARK: - Public Request Method + + /// 异步网络请求方法 + /// - Parameter parameters: 请求参数配置 + /// - Returns: 封装好的 Response 模型 + static func request(parameters: XSNetwork.Parameters) async -> XSNetwork.Response { + + // 1. Token 检查 (非注册接口且无 Token 时,先尝试获取) + if XSLoginManager.manager.token == nil && parameters.path != kXSRegisterPath { + _ = await self.requestToken() + } + + // 2. 执行请求 + let result = await provider.requestAsync(.request(parameters: parameters)) + + // 3. 结果处理 + switch result { + case .success(let response): + let code = response.statusCode + + // 4. Token 失效处理 (401, 402, 403) + if (code == 401 || code == 402 || code == 403) && parameters.path != kXSRegisterPath { + + // 如果是 402 且需要提示 + if code == 402 && parameters.isToast { + await MainActor.run { XSToast.show("network_error_1".localized) } + } + + // 尝试刷新 Token + let success = await self.requestToken() + if success { + // 刷新成功后重试当前请求 + return await self.request(parameters: parameters) + } + } + + // 5. 正常业务数据解析 + return self.handleResponse(response, parameters: parameters) + + case .failure(let error): + // 6. 网络连接层错误处理 + xsLog("Network Error: \(error)") + if parameters.isToast { + await MainActor.run { XSToast.show("network_error_2".localized) } + } + return self.createErrorResponse(msg: "network_error_2".localized) + } + } + + // MARK: - Private Helper Methods + + /// 处理响应数据 + private static func handleResponse(_ response: Moya.Response, parameters: XSNetwork.Parameters) -> XSNetwork.Response { + do { + let tempData = try response.mapString() + xsLog("Path: \(parameters.path)") + xsLog("Params: \(parameters.parameters ?? [:])") + + let networkResponse: XSNetwork.Response = self._deserialize(data: tempData) + + // 业务逻辑错误提示 (例如 code != 200) + if !networkResponse.isSuccess && parameters.isToast { + let msg = networkResponse.msg ?? "Error".localized + DispatchQueue.main.async { + XSToast.show(msg) + } + } + + return networkResponse + + } catch { + if parameters.isToast { + DispatchQueue.main.async { XSToast.show("Error".localized) } + } + return self.createErrorResponse(msg: "Error".localized) + } + } + + /// 解析与解密数据 + private static func _deserialize(data: String) -> XSNetwork.Response { + let decrypted = XSCryptorService.decrypt(data: data) + xsLog("Decrypted Response: \(decrypted)") + + var response = XSNetwork.Response.deserialize(from: decrypted) + response?.rawData = decrypted + + if let response = response { + return response + } else { + return self.createErrorResponse(msg: "Error".localized) + } + } + + /// 创建错误响应对象 + private static func createErrorResponse(code: Int = -1, msg: String) -> XSNetwork.Response { + var res = XSNetwork.Response() + res.code = code + res.msg = msg + return res + } + + // MARK: - Token Management + + /// 获取/刷新 Token + @discardableResult + @MainActor + static func requestToken() async -> Bool { + // 如果已经有一个刷新任务在进行,直接等待并返回它的结果 + if let existingTask = refreshTokenTask { + return await existingTask.value + } + + // 创建新的刷新任务,明确指定 _Concurrency.Task + let task = _Concurrency.Task { () -> Bool in + let param = XSNetwork.Parameters(path: kXSRegisterPath, isLoding: false, isToast: false) + let response: XSNetwork.Response = await self.request(parameters: param) + + if let token = response.data { + XSLoginManager.manager.setAccountToken(token) + _Concurrency.Task { + await XSLoginManager.manager.updateUserInfo() + } + return true + } + return false + } + + self.refreshTokenTask = task + + // 等待任务完成 + let result = await task.value + + // 任务完成后重置,以便后续再次刷新 + self.refreshTokenTask = nil + + return result + } +} + +// MARK: - Moya Async Extension +extension MoyaProvider { + /// 封装 Moya 为 Async/Await 风格 + func requestAsync(_ target: Target) async -> Result { + await withCheckedContinuation { continuation in + self.request(target) { result in + continuation.resume(returning: result) + } + } + } +} + +// MARK: - Backward Compatibility +extension XSNetwork { + /// 保留旧的闭包回调方法,内部调用 async 方法,方便平滑迁移 + static func request(parameters: XSNetwork.Parameters, completion: ((_ response: XSNetwork.Response) -> Void)?) { + _Concurrency.Task { + let response: XSNetwork.Response = await self.request(parameters: parameters) + await MainActor.run { + completion?(response) + } + } + } +} diff --git a/XSeri/Base/Networking/Base/XSNetworkModel.swift b/XSeri/Base/Networking/Base/XSNetworkModel.swift new file mode 100644 index 0000000..77e907d --- /dev/null +++ b/XSeri/Base/Networking/Base/XSNetworkModel.swift @@ -0,0 +1,75 @@ +// +// XSNetworkModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import Moya +import SmartCodable +import Alamofire + +extension XSNetwork { + + /// 业务成功状态码 + static let SuccessCode = 200 + + /// 请求参数配置模型 + struct Parameters { + var path: String + var parameters: [String : Any]? + var method: Moya.Method = .post + var isLoding: Bool = false + var isToast: Bool = true + + init(path: String, + parameters: [String : Any]? = nil, + method: Moya.Method = .post, + isLoding: Bool = false, + isToast: Bool = true) { + self.path = path + self.parameters = parameters + self.method = method + self.isLoding = isLoding + self.isToast = isToast + } + } + + /// 统一响应模型 + struct Response: SmartCodable { + var data: T? + var code: Int? + var msg: String? + + /// 原始解密数据,用于特殊处理 + @IgnoredKey + var rawData: Any? + + /// 是否请求成功(基于业务状态码) + var isSuccess: Bool { + return code == XSNetwork.SuccessCode + } + } + + /// 分页列表模型 + struct List: SmartCodable { + var list: [T]? + var pagination: Pagination? + } + + /// 分页信息模型 + struct Pagination: SmartCodable { + var current_page: Int? + var page_size: Int? + var page_total: Int? + var total_size: Int? + + /// SmartCodable 默认支持下划线转驼峰,但显式定义更安全 + enum CodingKeys: String, CodingKey { + case current_page = "current_page" + case page_size = "page_size" + case page_total = "page_total" + case total_size = "total_size" + } + } +} diff --git a/XSeri/Base/Networking/Base/XSNetworkMonitorManager.swift b/XSeri/Base/Networking/Base/XSNetworkMonitorManager.swift new file mode 100644 index 0000000..30329d5 --- /dev/null +++ b/XSeri/Base/Networking/Base/XSNetworkMonitorManager.swift @@ -0,0 +1,76 @@ +// +// XSNetworkMonitorManager.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import Network + +class XSNetworkMonitorManager: NSObject { + + static let manager = XSNetworkMonitorManager() + + ///是否有网 + var isReachable: Bool? + + private var connectionType: NWInterface.InterfaceType? + private var status: NWPath.Status? + + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "NetworkMonitorQueue") + + func startMonitoring() { + + monitor.pathUpdateHandler = { [weak self] path in + guard let self = self else { return } + self.status = path.status + + if path.usesInterfaceType(.wifi) { + self.connectionType = .wifi + } else if path.usesInterfaceType(.cellular) { + self.connectionType = .cellular + } else if path.usesInterfaceType(.wiredEthernet) { + self.connectionType = .wiredEthernet + } else { + self.connectionType = nil + } + + + if path.status == .satisfied, self.connectionType != nil { + if self.isReachable == false { + self.isReachable = true + DispatchQueue.main.async { + NotificationCenter.default.post(name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil) + } + } else { + self.isReachable = true + } + } else { + if self.isReachable == true { + self.isReachable = false + DispatchQueue.main.async { + NotificationCenter.default.post(name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil) + } + } else { + self.isReachable = false + } + } + } + + + monitor.start(queue: queue) + } + + func stopMonitoring() { + monitor.cancel() + } + +} + +extension XSNetworkMonitorManager { + ///网络发生变化 + static let networkStatusDidChangeNotification = Notification.Name(rawValue: "XSNetworkMonitorManager.networkStatusDidChangeNotification") +} + diff --git a/XSeri/Base/Networking/Base/XSNetworkPlugin.swift b/XSeri/Base/Networking/Base/XSNetworkPlugin.swift new file mode 100644 index 0000000..27dc1ce --- /dev/null +++ b/XSeri/Base/Networking/Base/XSNetworkPlugin.swift @@ -0,0 +1,42 @@ +// +// XSNetworkPlugin.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import Foundation +import Moya + +/// 网络请求加载指示器插件 +/// 用于处理请求过程中的 HUD 显示与隐藏,实现 UI 与网络逻辑解耦 +struct XSNetworkHUDPlugin: PluginType { + + func willSend(_ request: RequestType, target: TargetType) { + guard let target = target as? XSNetwork.Target, + case let .request(parameters) = target else { + return + } + + // 如果配置了需要 loading,则在主线程显示 HUD + if parameters.isLoding { + DispatchQueue.main.async { + XSHud.show() + } + } + } + + func didReceive(_ result: Result, target: TargetType) { + guard let target = target as? XSNetwork.Target, + case let .request(parameters) = target else { + return + } + + // 无论请求成功或失败,只要配置了 loading,均在结束时隐藏 HUD + if parameters.isLoding { + DispatchQueue.main.async { + XSHud.dismiss() + } + } + } +} diff --git a/XSeri/Base/Networking/Base/XSNetworkTarget.swift b/XSeri/Base/Networking/Base/XSNetworkTarget.swift new file mode 100644 index 0000000..3c77ac9 --- /dev/null +++ b/XSeri/Base/Networking/Base/XSNetworkTarget.swift @@ -0,0 +1,89 @@ +// +// XSNetworkTarget.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import Moya +import SmartCodable +import AdSupport +import YYCategories + +extension XSNetwork { + enum Target { + case request(parameters: XSNetwork.Parameters) + } +} + +extension XSNetwork.Target: TargetType { + var baseURL: URL { + return .init(string: XSBaseURL)! + } + + var path: String { + switch self { + case .request(let parameters): + return XSURLPathPrefix + parameters.path + } + } + + var method: Moya.Method { + switch self { + case .request(let parameters): + return parameters.method + } + } + + var task: Moya.Task { + switch self { + case .request(let parameters): + let params = parameters.parameters ?? [:] + return .requestParameters(parameters: params, encoding: getEncoding()) + } + } + + var headers: [String : String]? { + let userToken = XSLoginManager.manager.token?.token ?? "" + let dic: [String : String] = [ + "system-version" : kXSSystemVersion, + "lang-key" : "en", // 当前语言,后续可根据应用语言环境动态获取 + "time-zone" : String.timeZone(), // 时区 + "app-version" : kXSVersion, + "device-id" : XSDeviceId.shared.id, // 设备id + "brand" : "apple", // 品牌 + "app-name" : kXSBundleName, + "system-type" : "ios", + "idfa" : ASIdentifierManager.shared().advertisingIdentifier.uuidString, + "model" : UIDevice.current.machineModelName ?? "", + "authorization" : userToken, + "device-gaid" : UIDevice.current.identifierForVendor?.uuidString ?? "", + "product-prefix" : "XSeri" + ] + return dic + } + + var sampleData: Data { + return "".data(using: .utf8)! + } +} + +private extension XSNetwork.Target { + func getEncoding() -> ParameterEncoding { + switch self.method { + case .get, .delete: + return URLEncoding.default + default: + return JSONEncoding.default + } + } +} + +extension String { + static func timeZone() -> String { + let timeZone = NSTimeZone.local as NSTimeZone + let seconds = timeZone.secondsFromGMT / 3600 + let sign = seconds >= 0 ? "+" : "-" + return String(format: "GMT%@%02d:00", sign, abs(seconds)) + } +} diff --git a/XSeri/Base/Networking/Base/XSURLPath.swift b/XSeri/Base/Networking/Base/XSURLPath.swift new file mode 100644 index 0000000..8b3bd03 --- /dev/null +++ b/XSeri/Base/Networking/Base/XSURLPath.swift @@ -0,0 +1,25 @@ +// +// XSURLPath.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +let XSBaseURL = "https://api-breeltv.breeltv.com" +let XSURLPathPrefix = "/reel" +let XSWebBaseURL = "https://www.breeltv.com" +let XSCampaignWebURL = "https://campaign.breeltv.com" + +///用户协议 +let kXSUserAgreementWebUrl = XSWebBaseURL + "/user_policy" +///隐私协议 +let kXSPrivacyPolicyWebUrl = XSWebBaseURL + "/private" + +///反馈首页 +let kXSFeedBackHomeWebUrl = XSCampaignWebURL + "/pages/leave/index" +///反馈列表 +let kXSFeedBackListWebUrl = XSCampaignWebURL + "/pages/leave/list" +///反馈详情 +let kXSFeedBackDetailWebUrl = XSCampaignWebURL + "/pages/leave/detail" +///注销账号 +let kXSLogoutWebUrl = XSCampaignWebURL + "/pages/setting/logout" diff --git a/XSeri/Base/View/XSButton.swift b/XSeri/Base/View/XSButton.swift new file mode 100644 index 0000000..f5ebf32 --- /dev/null +++ b/XSeri/Base/View/XSButton.swift @@ -0,0 +1,56 @@ +// +// XSButton.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/25. +// + +import UIKit + +class XSButton: UIButton { + + override class var layerClass: AnyClass { + return CAGradientLayer.self + } + + var xs_gradientLayer: CAGradientLayer { + return self.layer as! CAGradientLayer + } + + var xs_colors: [CGColor]? { + get { + return xs_gradientLayer.colors as? [CGColor] + } + set { + xs_gradientLayer.colors = newValue + } + } + + var xs_startPoint: CGPoint { + get { + return xs_gradientLayer.startPoint + } + set { + xs_gradientLayer.startPoint = newValue + } + } + + var xs_endPoint: CGPoint { + get { + return xs_gradientLayer.endPoint + } + set { + xs_gradientLayer.endPoint = newValue + } + } + + var xs_locations: [NSNumber]? { + get { + return xs_gradientLayer.locations + } + set { + xs_gradientLayer.locations = newValue + } + } + +} diff --git a/XSeri/Base/View/XSCollectionView.swift b/XSeri/Base/View/XSCollectionView.swift new file mode 100644 index 0000000..a8ea821 --- /dev/null +++ b/XSeri/Base/View/XSCollectionView.swift @@ -0,0 +1,23 @@ +// +// XSCollectionView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +class XSCollectionView: UICollectionView { + + override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + self.backgroundColor = .clear + self.contentInsetAdjustmentBehavior = .never + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/XSeri/Base/View/XSCustomTabBar.swift b/XSeri/Base/View/XSCustomTabBar.swift new file mode 100644 index 0000000..79dd5c9 --- /dev/null +++ b/XSeri/Base/View/XSCustomTabBar.swift @@ -0,0 +1,20 @@ +// +// XSCustomTabBar.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/01/12. +// + +import UIKit +import ESTabBarController_swift + +final class XSCustomTabBar: ESTabBar { + + var contentHeight: CGFloat = 60 + + override func sizeThatFits(_ size: CGSize) -> CGSize { + var result = super.sizeThatFits(size) + result.height = contentHeight + XSScreen.safeBottom + return result + } +} diff --git a/XSeri/Base/View/XSImageView.swift b/XSeri/Base/View/XSImageView.swift new file mode 100644 index 0000000..c2b62cb --- /dev/null +++ b/XSeri/Base/View/XSImageView.swift @@ -0,0 +1,98 @@ +// +// XSImageView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import Kingfisher + +class XSImageView: UIImageView { + + var placeholderColor = UIColor.black + var placeholderImage = UIImage(named: "placeholder_image") + + private lazy var placeholderImageView: UIImageView = { + let imageView = UIImageView(image: placeholderImage) + imageView.isHidden = true + imageView.contentMode = .scaleAspectFit + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_init() + } + + override init(image: UIImage?) { + super.init(image: image) + xs_init() + } + + override init(image: UIImage?, highlightedImage: UIImage?) { + super.init(image: image, highlightedImage: highlightedImage) + xs_init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + xs_init() + } + + override func awakeFromNib() { + super.awakeFromNib() + xs_init() + } + + func xs_init() { + self.contentMode = .scaleAspectFill + self.layer.masksToBounds = true + if image == nil { + self.backgroundColor = self.placeholderColor + placeholderImageView.isHidden = false + } + addSubview(placeholderImageView) + } + + override var image: UIImage? { + didSet { + if self.backgroundColor == nil && image == nil { + self.backgroundColor = self.placeholderColor + } else if image != nil { + if self.backgroundColor == self.placeholderColor { + self.backgroundColor = nil + } + } + + if image == nil { + placeholderImageView.isHidden = false + } else { + placeholderImageView.isHidden = true + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + placeholderImageView.frame = .init(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height) + placeholderImageView.center = .init(x: self.bounds.width / 2, y: self.bounds.height / 2) + } + +} + +extension UIImageView { + func xs_setImage(_ url: String?, placeholder: UIImage? = nil, completer: ((_ image: UIImage?, _ url: URL?) -> Void)? = nil) { + + self.kf.setImage(with: URL(string: url ?? ""), placeholder: placeholder, options: nil) { result in + switch result { + case .success(let value): + completer?(value.image, value.source.url) + default : + completer?(nil, nil) + break + } + } + } +} diff --git a/XSeri/Base/View/XSLabel.swift b/XSeri/Base/View/XSLabel.swift new file mode 100644 index 0000000..7354e85 --- /dev/null +++ b/XSeri/Base/View/XSLabel.swift @@ -0,0 +1,56 @@ +// +// XSLabel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit + +class XSLabel: UILabel { + + var textColors: [CGColor]? + var textStartPoint: CGPoint? + var textEndPoint: CGPoint? + + var colorImage: UIImage? + + + override func layoutSubviews() { + super.layoutSubviews() + let size = self.bounds.size + if let text = self.text, text.count > 0, let colors = self.textColors, let startPoint = self.textStartPoint, let endPoine = self.textEndPoint { + self.textColor = UIColor(patternImage: UIImage.xs_getGradientImage(size: size, colors: colors, startPoint: startPoint, endPoint: endPoine)) + } else if let image = self.colorImage { + self.textColor = UIColor(patternImage: image.xs_resized(to: size)) + } + } + +} + +extension UIImage { + + static func xs_getGradientImage(size: CGSize, colors: [CGColor], startPoint: CGPoint, endPoint: CGPoint) -> UIImage { + + UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale) + guard let context = UIGraphicsGetCurrentContext() else{return UIImage()} + let colorSpace = CGColorSpaceCreateDeviceRGB() + ///设置渐变颜色 + let gradientRef = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil)! + let startPoint = CGPoint(x: size.width * startPoint.x, y: size.height * startPoint.y) + let endPoint = CGPoint(x: size.width * endPoint.x, y: size.height * endPoint.y) + context.drawLinearGradient(gradientRef, start: startPoint, end: endPoint, options: CGGradientDrawingOptions(arrayLiteral: .drawsBeforeStartLocation,.drawsAfterEndLocation)) + let gradientImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return gradientImage ?? UIImage() + } + + func xs_resized(to size: CGSize) -> UIImage { + let renderer = UIGraphicsImageRenderer(size: size) + return renderer.image { _ in + self.draw(in: CGRect(origin: .zero, size: size)) + } + } + +} + diff --git a/XSeri/Base/View/XSPanModalContentView.swift b/XSeri/Base/View/XSPanModalContentView.swift new file mode 100644 index 0000000..39e622a --- /dev/null +++ b/XSeri/Base/View/XSPanModalContentView.swift @@ -0,0 +1,76 @@ +// +// XSPanModalContentView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/26. +// + +import UIKit +import HWPanModal + +class XSPanModalContentView: HWPanModalContentView { + + var contentHeight = XSScreen.height * (2 / 3) + + var mainScrollView: UIScrollView? + + ///更新UI contentSize发生变化时调用 + func setNeedsLayoutUpdate() { + self.panModalSetNeedsLayoutUpdate() + } + + + override init(frame: CGRect) { + super.init(frame: frame) + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: HWPanModalPresentable + override func panScrollable() -> UIScrollView? { + return mainScrollView + } + + override func longFormHeight() -> PanModalHeight { + return PanModalHeightMake(.content, contentHeight) + } + + override func showDragIndicator() -> Bool { + return false + } + + override func backgroundConfig() -> HWBackgroundConfig { + let config = HWBackgroundConfig() + config.backgroundAlpha = 0.6 + return config + } + + override func allowsTapBackgroundToDismiss() -> Bool { + return true + } + + override func allowsDragToDismiss() -> Bool { + return false + } + + override func allowsPullDownWhenShortState() -> Bool { + return false + } + + override func showsScrollableVerticalScrollIndicator() -> Bool { + return false + } + + override func springDamping() -> CGFloat { + return 1 + } + + override func cornerRadius() -> CGFloat { + return 18 + } + +} diff --git a/XSeri/Base/View/XSProgressView.swift b/XSeri/Base/View/XSProgressView.swift new file mode 100644 index 0000000..d5d0d86 --- /dev/null +++ b/XSeri/Base/View/XSProgressView.swift @@ -0,0 +1,244 @@ +// +// XSProgressView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import YYCategories +import YYText + +class XSProgressView: UIView { + + ///滑动开始 + var panStart: (() -> Void)? + + ///滑动中 + var panChange: ((_ progress: CGFloat) -> Void)? + + ///滑动完成回调 + var panFinish: ((_ progress: CGFloat) -> Void)? + + var progress: CGFloat = 0 { + didSet { + if !isPaning { + setNeedsDisplay() + } + } + } + + ///用来记录滑动时的当前进度 + private var tempProgress: CGFloat = 0 + + ///滑动进度 + private var panProgress: CGFloat = 0 + + var progressColor: UIColor = .FFDAA_4.withAlphaComponent(0.39) { + didSet { + setNeedsDisplay() + } + } + var currentProgress: UIColor = .FFDAA_4 { + didSet { + setNeedsDisplay() + } + } + + var lineWidth: CGFloat = 2 { + didSet { + setNeedsDisplay() + } + } + + ///加载中状态 + var isLoading = false { + didSet { + if isLoading { + if gradientTimer == nil { + gradientTimer = Timer.scheduledTimer(timeInterval: 0.05, target: YYTextWeakProxy(target: self), selector: #selector(handleGradientTimer), userInfo: nil, repeats: true) + } + } else { + gradientTimer?.invalidate() + gradientTimer = nil + } + } + } + + var thumbImage: UIImage? { + didSet { + setNeedsDisplay() + } + } + + var insets: UIEdgeInsets = .init(top: 0, left: 15, bottom: 0, right: 15) { + didSet { + self.invalidateIntrinsicContentSize() + setNeedsDisplay() + } + } + + private(set) lazy var panGesture: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:))) + return pan + }() + + private(set) lazy var tagGesture: UITapGestureRecognizer = { + let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:))) + return tap + }() + + ///是否在滑动中 + private var isPaning: Bool = false + + private var gradientTimer: Timer? + + private var gradientValue: CGFloat = 0 + + override var intrinsicContentSize: CGSize { + return .init(width: XSScreen.width, height: lineWidth + insets.top + insets.bottom) + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .clear + + self.addGestureRecognizer(panGesture) + self.addGestureRecognizer(tagGesture) + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + setNeedsDisplay() + } + + @objc private func handleGradientTimer() { + gradientValue += 0.1 + if gradientValue > 1 { + gradientValue = 0 + } + setNeedsDisplay() + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + guard let context = UIGraphicsGetCurrentContext() else { return } + let width = rect.width + let thumbImageSize = self.thumbImage?.size ?? .zero + + let progressX = insets.left + thumbImageSize.width / 2 + let progressY = insets.top + let progressWidth = width - insets.left - insets.right - thumbImageSize.width + + if isLoading, !isPaning { + // 定义颜色空间 + let colorSpace = CGColorSpaceCreateDeviceRGB() + let colors: [CGColor] = [ + UIColor.clear.cgColor, + UIColor.white.cgColor, + UIColor.clear.cgColor + ] + let locations: [CGFloat] = [0.0, gradientValue, 1.0] + + guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: locations) else { + return + } + + let gradientRect = CGRect(x: progressX, + y: progressY, + width: progressWidth, + height: lineWidth) + + // 定义渐变的起点和终点 + let startPoint = CGPoint(x: rect.minX, y: rect.minY) + let endPoint = CGPoint(x: rect.maxX, y: rect.maxY) + + // 裁剪到渐变区域 + context.saveGState() + context.clip(to: gradientRect) + + // 绘制渐变 + context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) + } else { + var progress = self.progress + if self.isPaning { + progress = self.panProgress + } + if progress < 0 { + progress = 0 + } + if progress > 1 { + progress = 1 + } + + + ///绘制进度 + let progressPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth, height: lineWidth), cornerRadius: lineWidth / 2) + context.addPath(progressPath.cgPath) + context.setFillColor(progressColor.cgColor) + context.fillPath() + + ///绘制当前进度 + let currentPath = UIBezierPath(roundedRect: CGRect(x: progressX, y: progressY, width: progressWidth * progress, height: lineWidth), cornerRadius: lineWidth / 2) + context.addPath(currentPath.cgPath) + context.setFillColor(currentProgress.cgColor) + context.fillPath() + + if let thumbImage = thumbImage { + let size = thumbImage.size + let frame = CGRect(x: progressWidth * progress - size.width / 2 + progressX, y: progressY - size.width / 2 + lineWidth / 2, width: size.width, height: size.height) + + thumbImage.draw(in: frame) + } + } + + + } + +} + +extension XSProgressView { + + @objc func handlePanGesture(sender: UIPanGestureRecognizer) { + + switch sender.state { + case .began: + self.isPaning = true + self.tempProgress = self.progress + sender.setTranslation(CGPoint(x: 0, y: 0), in: self) + self.panStart?() + + case .changed: + let point = sender.translation(in: self) + let offsetX = point.x / (self.width - self.insets.left - self.insets.right) + self.panProgress = self.tempProgress + offsetX + if self.panProgress < 0 { + self.panProgress = 0 + } + if self.panProgress > 1 { + self.panProgress = 1 + } + self.panChange?(self.panProgress) + setNeedsDisplay() + + default: + self.isPaning = false + self.panFinish?(self.panProgress) + + self.panProgress = 0 + } + } + + @objc func handleTapGesture(sender: UITapGestureRecognizer) { + let point = sender.location(in: self) + let offsetX = (point.x - self.insets.left) / (self.width - self.insets.left - self.insets.right) + self.panFinish?(offsetX) + } +} diff --git a/XSeri/Base/View/XSTabBarItemContentView.swift b/XSeri/Base/View/XSTabBarItemContentView.swift new file mode 100644 index 0000000..f63e053 --- /dev/null +++ b/XSeri/Base/View/XSTabBarItemContentView.swift @@ -0,0 +1,83 @@ +import UIKit +import ESTabBarController_swift + +class XSTabBarItemContentView: ESTabBarItemContentView { + + override init(frame: CGRect) { + super.init(frame: frame) + + // 设置未选中状态的颜色 + textColor = XSConfig.Color.tabBarInactive + iconColor = XSConfig.Color.tabBarInactive + + // 设置选中状态的颜色 + highlightTextColor = XSConfig.Color.tabBarActive + highlightIconColor = XSConfig.Color.tabBarActive + + // 允许标题不压缩,解决省略号问题 + titleLabel.numberOfLines = 1 + titleLabel.textAlignment = .center + + // 禁止 ImageView 拉伸 + imageView.contentMode = .scaleAspectFit + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLayout() { + super.updateLayout() + + // 1. 处理图标大小 (根据 Figma,选中 36pt,未选中 24pt) + let iconSize: CGFloat = selected ? 36.0 : 24.0 + // 调整纵向位置,确保视觉居中 + let iconY: CGFloat = selected ? 6.0 : 10.0 + imageView.frame = CGRect(x: (bounds.size.width - iconSize) / 2.0, + y: iconY, + width: iconSize, + height: iconSize) + + // 2. 处理文字布局,防止省略号 + titleLabel.font = selected ? XSConfig.Font.tabBarActive : XSConfig.Font.tabBarInactive + titleLabel.sizeToFit() + // 给予足够的宽度以防被截断 + let labelWidth = bounds.size.width + 20 + titleLabel.frame = CGRect(x: (bounds.size.width - labelWidth) / 2.0, + y: imageView.frame.maxY + 2.0, + width: labelWidth, + height: 14.0) + } + + override func selectAnimation(animated: Bool, completion: (() -> Void)?) { + super.selectAnimation(animated: animated, completion: completion) + + if animated { + // 执行平滑放大动画 + UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut], animations: { + self.updateLayout() + }, completion: { _ in + completion?() + }) + } else { + self.updateLayout() + completion?() + } + } + + override func deselectAnimation(animated: Bool, completion: (() -> Void)?) { + super.deselectAnimation(animated: animated, completion: completion) + + if animated { + // 执行平滑缩小还原动画 + UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn], animations: { + self.updateLayout() + }, completion: { _ in + completion?() + }) + } else { + self.updateLayout() + completion?() + } + } +} diff --git a/XSeri/Base/View/XSTableView.swift b/XSeri/Base/View/XSTableView.swift new file mode 100644 index 0000000..e5ba274 --- /dev/null +++ b/XSeri/Base/View/XSTableView.swift @@ -0,0 +1,49 @@ +// +// XSTableView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit + +class XSTableView: UITableView { + + var insetGroupedMargins: CGFloat = 15 + + override init(frame: CGRect, style: UITableView.Style) { + super.init(frame: frame, style: style) + separatorColor = .white.withAlphaComponent(0.1) + separatorInset = .init(top: 0, left: 16, bottom: 0, right: 16) + self.backgroundColor = .clear + self.contentInsetAdjustmentBehavior = .never + + if style == .insetGrouped { + sectionFooterHeight = 14 + sectionHeaderHeight = 0.1 + } else if style == .plain { + if #available(iOS 15.0, *) { + sectionHeaderTopPadding = 0 + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var layoutMargins: UIEdgeInsets { + set { + super.layoutMargins = newValue + } + get { + var margins = super.layoutMargins + if self.style == .insetGrouped { + margins.left = self.safeAreaInsets.left + insetGroupedMargins + margins.right = self.safeAreaInsets.right + insetGroupedMargins + } + return margins + } + } + +} diff --git a/XSeri/Base/View/XSTableViewCell.swift b/XSeri/Base/View/XSTableViewCell.swift new file mode 100644 index 0000000..cd78d2f --- /dev/null +++ b/XSeri/Base/View/XSTableViewCell.swift @@ -0,0 +1,49 @@ +// +// XSTableViewCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit + +class XSTableViewCell: UITableViewCell { + + private(set) lazy var xs_indicatorImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right_icon_01")) + return imageView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.layer.rasterizationScale = UIScreen.main.scale + self.layer.shouldRasterize = true + self.selectionStyle = .none + self.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + +} + +extension UITableViewCell { + + var br_tableView: UITableView? { + return self.value(forKey: "_tableView") as? UITableView + } +} + diff --git a/XSeri/Base/View/XSView.swift b/XSeri/Base/View/XSView.swift new file mode 100644 index 0000000..1fbb82e --- /dev/null +++ b/XSeri/Base/View/XSView.swift @@ -0,0 +1,56 @@ +// +// XSView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/25. +// + +import UIKit + +class XSView: UIView { + + override class var layerClass: AnyClass { + return CAGradientLayer.self + } + + var xs_gradientLayer: CAGradientLayer { + return self.layer as! CAGradientLayer + } + + var xs_colors: [CGColor]? { + get { + return xs_gradientLayer.colors as? [CGColor] + } + set { + xs_gradientLayer.colors = newValue + } + } + + var xs_startPoint: CGPoint { + get { + return xs_gradientLayer.startPoint + } + set { + xs_gradientLayer.startPoint = newValue + } + } + + var xs_endPoint: CGPoint { + get { + return xs_gradientLayer.endPoint + } + set { + xs_gradientLayer.endPoint = newValue + } + } + + var xs_locations: [NSNumber]? { + get { + return xs_gradientLayer.locations + } + set { + xs_gradientLayer.locations = newValue + } + } + +} diff --git a/XSeri/Base/WebView/XSAppWebViewController.swift b/XSeri/Base/WebView/XSAppWebViewController.swift new file mode 100644 index 0000000..de93827 --- /dev/null +++ b/XSeri/Base/WebView/XSAppWebViewController.swift @@ -0,0 +1,78 @@ +// +// XSAppWebViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/28. +// + +import UIKit + +class XSAppWebViewController: XSBaseWebViewController { + + var id: String? + + private var receiveDataCount = 0 + + var theme: String? = "theme_3" + + override func viewDidLoad() { + super.viewDidLoad() + self.autoTitle = false + + if webUrl == kXSFeedBackListWebUrl { + self.title = "feedback_history".localized + } else if webUrl == kXSFeedBackHomeWebUrl { + self.title = "feedback".localized + } else if webUrl == kXSFeedBackDetailWebUrl { + self.title = "feedback_detail".localized + } else if webUrl == kXSLogoutWebUrl { + self.title = "account_deletion".localized + } + + } + + override func xs_webViewDidFinishLoad(_ webView: XSWebView) { + super.xs_webViewDidFinishLoad(webView) + receiveDataCount = 0 + receiveDataFromNative() + } + +} + +extension XSAppWebViewController { + + func receiveDataFromNative() { + receiveDataCount += 1 + if receiveDataCount > 10 { return } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + var dic = [ + "token" : XSLoginManager.manager.token?.token ?? "", + "time_zone" : String.timeZone(), + "lang" : "en", + "type" : "ios", + "device-id" : XSDeviceId.shared.id + ] + + if let theme = theme { + dic["theme"] = theme + } + + if let id = id { + dic["id"] = id + } + + if let json = dic.toJsonString() { + let js = "receiveDataFromNative(\(json))" + self.webView.evaluateJavaScript(js) { [weak self] _, error in + guard let self = self else { return } + if error != nil { + self.receiveDataFromNative() + } + } + } + } + + } +} diff --git a/XSeri/Base/WebView/XSBaseWebViewController+Script.swift b/XSeri/Base/WebView/XSBaseWebViewController+Script.swift new file mode 100644 index 0000000..c341af1 --- /dev/null +++ b/XSeri/Base/WebView/XSBaseWebViewController+Script.swift @@ -0,0 +1,111 @@ +// +// XSBaseWebViewController+Script.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/28. +// + +import UIKit +import WebKit +import ZLPhotoBrowser +import SmartCodable + + +///APP交互 +let kXSWebMessageAPP = "js2app" +///打开反馈列表 +let kXSWebMessageOpenFeedbackList = "openFeedbackList" +///打开反馈详情 +let kXSWebMessageOpenFeedbackDetail = "openFeedbackDetail" +///打开相册 +let kXSWebMessageOpenPhotoPicker = "openPhotoPicker" +///删除账号成功 +let kXSWebMessageAccountDeletionFinish = "accountLogout" + +extension XSBaseWebViewController { + + func xs_webViewUserContentController(didReceive message: WKScriptMessage) { + let name = message.name + let body = message.body + + switch name { + case kXSWebMessageOpenFeedbackList: + let vc = XSAppWebViewController() + vc.webUrl = kXSFeedBackListWebUrl + self.navigationController?.pushViewController(vc, animated: true) + + case kXSWebMessageOpenFeedbackDetail: + guard let body = body as? [String : Any] else { return } + guard let id = body["id"] as? Int else { return } + + let vc = XSAppWebViewController() + vc.id = "\(id)" + vc.webUrl = kXSFeedBackDetailWebUrl + self.navigationController?.pushViewController(vc, animated: true) + + case kXSWebMessageOpenPhotoPicker: + openPhotoPicker() + + case kXSWebMessageAPP: + guard let body = message.body as? [String : Any] else { return } + guard let model = XSWebMessageModel.deserialize(from: body) else { return } + let type = model.type + let data = model.data + + if type == "login" { +// let view = FALoginView() +// view.present(in: nil) + + } else if type == "open_notify" { +// openNotify() + + } else if type == "watch_video" { + let vc = XSShortDetailViewController() + vc.shortId = data?.short_play_id + vc.activityId = data?.activity_id + self.navigationController?.pushViewController(vc, animated: true) + } else { + + guard let urlStr = data?.link else { return } + guard let url = URL(string: urlStr) else { return } + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + + case kXSWebMessageAccountDeletionFinish: + self.navigationController?.popToRootViewController(animated: true) + NotificationCenter.default.post(name: XSLoginManager.loginStateDidChangeNotification, object: nil) + + default: + break + } + + } + + + ///打开相册 + private func openPhotoPicker() { + + ZLPhotoConfiguration.default().allowSelectOriginal = false + ZLPhotoConfiguration.default().maxSelectCount = 1 + ZLPhotoConfiguration.default().allowEditImage = false + ZLPhotoConfiguration.default().allowSelectVideo = false + ZLPhotoConfiguration.default().allowSelectGif = false + ZLPhotoConfiguration.default().allowTakePhotoInLibrary = false + + let picker = ZLPhotoPicker() + picker.selectImageBlock = { [weak self] (results, _) in + guard let self = self else { return } + guard let image = results.first?.image else { return } + guard let imageData = image.jpegData(compressionQuality: 0.8) else { return } + let imageDataStr = imageData.base64EncodedString(options: .endLineWithCarriageReturn) + + let js = "uploadConvertImage('\(imageDataStr)')" + self.webView.evaluateJavaScript(js) + } + + picker.showPhotoLibrary(sender: self) + } + +} diff --git a/XSeri/Base/WebView/XSBaseWebViewController.swift b/XSeri/Base/WebView/XSBaseWebViewController.swift new file mode 100644 index 0000000..41c0b5f --- /dev/null +++ b/XSeri/Base/WebView/XSBaseWebViewController.swift @@ -0,0 +1,113 @@ +// +// XSBaseWebViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import WebKit +import SnapKit + +class XSBaseWebViewController: XSViewController { + + var webUrl: String? + + ///自动设置标题 + var autoTitle = true + + var needAutoRefresh = true + + private(set) lazy var webView: XSWebView = { + let controller = WKUserContentController() + + let config = WKWebViewConfiguration() + config.userContentController = controller + config.defaultWebpagePreferences.allowsContentJavaScript = true + config.preferences.javaScriptCanOpenWindowsAutomatically = false + let webView = XSWebView(frame: self.view.bounds, configuration: config) + webView.delegate = self + return webView + }() + + override func viewDidLoad() { + super.viewDidLoad() +// self.edgesForExtendedLayout = [] + + xs_setupUI() + + if let url = webUrl { + self.load(webUrl: url) + } + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(false, animated: true) + self.xs_setNavigationStyle() + } + + func load(webUrl: String) { + let str: String = webUrl + + guard let url = URL(string: str) else { return } + let request = URLRequest(url: url, timeoutInterval: 30) + + self.webView.load(request) + } + + func reload() { + self.webView.reload() + } + +} + +extension XSBaseWebViewController { + + private func xs_setupUI() { + self.view.addSubview(webView) + + self.webView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(0) + make.right.equalToSuperview().offset(0) + make.bottom.equalToSuperview().offset(0) + make.top.equalTo(self.view.safeAreaLayoutGuide) + } + } + + +} + +//MARK: -------------- VPWebViewDelegate -------------- +extension XSBaseWebViewController: XSWebViewDelegate { + + func xs_webView(_ webView: XSWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool { + self.webView.isHidden = false + return true + } + + func xs_webViewDidStartLoad(_ webView: XSWebView) { + XSHud.show(containerView: self.view) + } + + func xs_webView(webView: XSWebView, didChangeTitle title: String) { + if autoTitle { + self.title = title + } + } + + func xs_webViewDidFinishLoad(_ webView: XSWebView) { + self.webView.isHidden = false + XSHud.dismiss() + } + + func xs_webView(_ webView: XSWebView, didFailLoadWithError error: any Error) { + XSHud.dismiss() + } + + func xs_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + self.xs_webViewUserContentController(didReceive: message) + } + +} diff --git a/XSeri/Base/WebView/XSWebMessageModel.swift b/XSeri/Base/WebView/XSWebMessageModel.swift new file mode 100644 index 0000000..6d8f1e8 --- /dev/null +++ b/XSeri/Base/WebView/XSWebMessageModel.swift @@ -0,0 +1,22 @@ +// +// XSWebMessageModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/28. +// + +import UIKit +import SmartCodable + +struct XSWebMessageModel: SmartCodable { + var type: String? + + var data: XSWebMessageData? +} + +struct XSWebMessageData: SmartCodable { + + var activity_id: String? + var short_play_id: String? + var link: String? +} diff --git a/XSeri/Base/WebView/XSWebView.swift b/XSeri/Base/WebView/XSWebView.swift new file mode 100644 index 0000000..9170493 --- /dev/null +++ b/XSeri/Base/WebView/XSWebView.swift @@ -0,0 +1,153 @@ +// +// XSWebView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import WebKit +import YYText + +//MARK:-------------- VPWebViewDelegate -------------- +@objc protocol XSWebViewDelegate: NSObjectProtocol { + + @objc optional func xs_webView(_ webView: XSWebView, shouldStartLoadWith navigationAction: WKNavigationAction) -> Bool + + @objc optional func xs_webViewDidStartLoad(_ webView: XSWebView) + + @objc optional func xs_webViewDidFinishLoad(_ webView: XSWebView) + + @objc optional func xs_webView(_ webView: XSWebView, didFailLoadWithError error: Error) + + ///进度 + @objc optional func xs_webView(webView: XSWebView, didChangeProgress progress: CGFloat) + ///标题 + @objc optional func xs_webView(webView: XSWebView, didChangeTitle title: String) + + ///web交互用 + @objc optional func xs_userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) + +} + +class XSWebView: WKWebView { + weak var delegate: XSWebViewDelegate? + + private(set) var scriptMessageHandlerArray: [String] = [ + kXSWebMessageAPP, + kXSWebMessageOpenFeedbackList, + kXSWebMessageOpenFeedbackDetail, + kXSWebMessageOpenPhotoPicker, + kXSWebMessageAccountDeletionFinish, + ] + + + deinit { + self.removeObserver(self, forKeyPath: "estimatedProgress") + self.removeObserver(self, forKeyPath: "title") + + } + + override init(frame: CGRect, configuration: WKWebViewConfiguration) { + super.init(frame: frame, configuration: configuration) + addScriptMessageHandler() + _setupInit() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + private func _setupInit() { + + self.isOpaque = false + self.navigationDelegate = self + self.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) + self.addObserver(self, forKeyPath: "title", options: .new, context: nil) + + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if object as? XSWebView == self { + if keyPath == "estimatedProgress", let progress = change?[NSKeyValueChangeKey.newKey] as? CGFloat { + self.delegate?.xs_webView?(webView: self, didChangeProgress: progress) + } else if keyPath == "title", let title = change?[NSKeyValueChangeKey.newKey] as? String { + self.delegate?.xs_webView?(webView: self, didChangeTitle: title) + } + } + } + + func load(urlStr: String) { + guard let url = URL(string: urlStr) else { return } + let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30) + self.load(request) + } + + func removeScriptMessageHandler() { + self.scriptMessageHandlerArray.forEach{ + configuration.userContentController.removeScriptMessageHandler(forName: $0) + } + } + func addScriptMessageHandler() { + self.scriptMessageHandlerArray.forEach{ + configuration.userContentController.add(YYTextWeakProxy(target: self) as! WKScriptMessageHandler, name: $0) + } + } +} + +//MARK:-------------- WKNavigationDelegate -------------- +extension XSWebView: WKNavigationDelegate { + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + + decisionHandler(.allow); + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + if let url = navigationAction.request.url, + url.scheme != "http", + url.scheme != "https" + { + UIApplication.shared.open(url) + decisionHandler(.cancel) + return + } + + if let result = self.delegate?.xs_webView?(self, shouldStartLoadWith: navigationAction) { + if result { + decisionHandler(.allow) + } else { + decisionHandler(.cancel) + } + } else { + decisionHandler(.allow) + } + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + self.delegate?.xs_webViewDidStartLoad?(self) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.delegate?.xs_webViewDidFinishLoad?(self) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + self.delegate?.xs_webView?(self, didFailLoadWithError: error) + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + self.delegate?.xs_webView?(self, didFailLoadWithError: error) + } + + +} + +//MARK:-------------- WKScriptMessageHandler -------------- +extension XSWebView: WKScriptMessageHandler { + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + self.delegate?.xs_userContentController?(userContentController, didReceive: message) + } + +} + diff --git a/XSeri/Class/Discover/Controller/XSDiscoverViewController.swift b/XSeri/Class/Discover/Controller/XSDiscoverViewController.swift new file mode 100644 index 0000000..68b2672 --- /dev/null +++ b/XSeri/Class/Discover/Controller/XSDiscoverViewController.swift @@ -0,0 +1,76 @@ +// +// XSDiscoverViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import JXPlayer + +class XSDiscoverViewController: JXPlayerListViewController { + + override var contentSize: CGSize { + return .init(width: XSScreen.width, height: XSScreen.height) + } + + override var ViewModelClass: JXPlayerListViewModel.Type { + return XSDiscoverViewModel.self + } + + var xs_viewModel: XSDiscoverViewModel { + return self.viewModel as! XSDiscoverViewModel + } + + deinit { + + } + + override func viewDidLoad() { + super.viewDidLoad() + self.register(XSDiscoverPlayerCell.self, forCellWithReuseIdentifier: "cell") + self.delegate = self + self.dataSource = self + + Task { + await self.xs_viewModel.requestDataArr(page: 1) + } + + } + + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.viewModel.currentCell?.pause() + } + +} + + +//MARK: JXPlayerListViewControllerDataSource +extension XSDiscoverViewController: JXPlayerListViewControllerDataSource { + func jx_playerListViewController(_ viewController: JXPlayerListViewController, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = self.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSDiscoverPlayerCell + cell.model = xs_viewModel.dataArr[indexPath.row] + return cell + } + + func jx_playerListViewController(_ viewController: JXPlayerListViewController, numberOfItemsInSection section: Int) -> Int { + return xs_viewModel.dataArr.count + } + +} + +//MARK: JXPlayerListViewControllerDelegate +extension XSDiscoverViewController: JXPlayerListViewControllerDelegate { + func jx_playerViewControllerLoadMoreData(playerViewController: JXPlayerListViewController) { + Task { + await self.xs_viewModel.requestDataArr(page: self.xs_viewModel.page + 1) + } + } +} diff --git a/XSeri/Class/Discover/View/XSDiscoverControlView.swift b/XSeri/Class/Discover/View/XSDiscoverControlView.swift new file mode 100644 index 0000000..cf5d339 --- /dev/null +++ b/XSeri/Class/Discover/View/XSDiscoverControlView.swift @@ -0,0 +1,201 @@ +// +// XSDiscoverControlView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import JXPlayer +import SnapKit + +class XSDiscoverControlView: JXPlayerListControlView { + + + override var model: Any? { + didSet { + let model = self.model as? XSShortModel + let videoInfo = model?.video_info + + epView.currentEp = videoInfo?.episode ?? "" + epView.totalEp = "\(model?.episode_total ?? 0)" + + collectButton.setNeedsUpdateConfiguration() + } + } + + override var viewModel: JXPlayerListViewModel? { + didSet { + self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil) + } + } + + override var durationTime: TimeInterval { + didSet { + updateProgress() + } + } + + override var currentTime: TimeInterval { + didSet { + updateProgress() + } + } + + override var isCurrent: Bool { + didSet { + updatePlayIconState() + } + } + + private lazy var progressView: XSProgressView = { + let view = XSProgressView() + view.insets = .init(top: 10, left: 16, bottom: 10, right: 16) + view.panFinish = { [weak self] progress in + guard let self = self else { return } + self.viewModel?.seekTo(Float(progress)) + } + return view + }() + + private lazy var epView: XSPlayerEpButton = { + let view = XSPlayerEpButton() + view.addAction(UIAction(handler: { [weak self] _ in + guard let self = self else { return } + let vc = XSShortDetailViewController() + vc.shortId = (self.model as? XSShortModel)?.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + }), for: .touchUpInside) + return view + }() + + private lazy var collectButton: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.contentInsets = .zero + configuration.imagePadding = 2 + configuration.imagePlacement = .top + configuration.attributedTitle = AttributedString("Save".localized, attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 12, weight: .bold), + .foregroundColor : UIColor.FFDAA_4 + ])) + + let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + guard let model = self.model as? XSShortModel else { return } + let isCollect = !(model.is_collect ?? false) + + Task { + await XSVideoAPI.requestCollectShort(isCollect: isCollect, shortId: model.short_play_id ?? "0", videoId: model.video_info?.short_play_video_id ?? "0") + } + })) + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + let model = self.model as? XSShortModel + let videoInfo = model?.video_info + + if model?.is_collect == true { + button.configuration?.image = UIImage(named: "collect_icon_01_selected") + } else { + button.configuration?.image = UIImage(named: "collect_icon_01") + } + } + return button + }() + + private lazy var playIconImageView = UIImageView() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: XSVideoAPI.updateShortCollectStateNotification, object: nil) + + xs_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "isPlaying" { + self.updatePlayIconState() + } + } + + private func updateProgress() { + if durationTime == 0 || currentTime == 0 { + progressView.progress = 0 + return + } + progressView.progress = currentTime / durationTime + } + + override func singleTapEvent() { + super.singleTapEvent() + self.viewModel?.userSwitchPlayAndPause() + } + + private func updatePlayIconState() { + let model = self.model as? XSShortModel + let videoInfo = model?.video_info + + if videoInfo?.is_lock == true { + playIconImageView.isHidden = true + } else { + playIconImageView.isHidden = false + if isCurrent == true, self.viewModel?.isPlaying != true { + playIconImageView.image = UIImage(named: "play_icon_01") + } else { + playIconImageView.image = UIImage(named: "pause_icon_01") + } + } + } + + @objc private func updateShortCollectStateNotification(sender: Notification) { + let model = self.model as? XSShortModel + + guard let userInfo = sender.userInfo else { return } + guard let shortPlayId = userInfo["id"] as? String else { return } + guard let state = userInfo["state"] as? Bool else { return } + guard shortPlayId == model?.short_play_id else { return } + model?.is_collect = state + self.collectButton.setNeedsUpdateConfiguration() + } +} + +extension XSDiscoverControlView { + + private func xs_setupUI() { + addSubview(progressView) + addSubview(epView) + addSubview(collectButton) + addSubview(playIconImageView) + + progressView.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.equalToSuperview().offset(-(XSScreen.customTabBarHeight)) + } + + epView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.centerX.equalToSuperview() + make.bottom.equalTo(progressView.snp.top).offset(-2) + } + + collectButton.snp.makeConstraints { make in + make.bottom.equalTo(epView.snp.top).offset(-18) + make.right.equalToSuperview().offset(-16) + } + + playIconImageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview() + } + + } + +} diff --git a/XSeri/Class/Discover/View/XSDiscoverPlayerCell.swift b/XSeri/Class/Discover/View/XSDiscoverPlayerCell.swift new file mode 100644 index 0000000..67ad923 --- /dev/null +++ b/XSeri/Class/Discover/View/XSDiscoverPlayerCell.swift @@ -0,0 +1,28 @@ +// +// XSDiscoverPlayerCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import JXPlayer + +class XSDiscoverPlayerCell: JXPlayerListCell { + + + override var ControlViewClass: JXPlayerListControlView.Type { + return XSDiscoverControlView.self + } + + + override var model: Any? { + didSet { + let model = self.model as? XSShortModel + let videoInfo = model?.video_info + + self.player.setPlayUrl(url: videoInfo?.video_url ?? "") + self.player.coverImageView?.xs_setImage(model?.image_url) + } + } +} diff --git a/XSeri/Class/Discover/ViewModel/XSDiscoverViewModel.swift b/XSeri/Class/Discover/ViewModel/XSDiscoverViewModel.swift new file mode 100644 index 0000000..7ec45e4 --- /dev/null +++ b/XSeri/Class/Discover/ViewModel/XSDiscoverViewModel.swift @@ -0,0 +1,54 @@ +// +// XSDiscoverViewModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import JXPlayer + +class XSDiscoverViewModel: JXPlayerListViewModel { + + private(set) var dataArr: [XSShortModel] = [] + private(set) var page = 1 + + @MainActor + private func addDataArr(dataArr: [XSShortModel]) { + guard dataArr.count > 0 else { return } + + var indexPaths: [IndexPath] = [] + var startRow = self.dataArr.count + + dataArr.forEach { _ in + indexPaths.append(IndexPath(row: startRow, section: 0)) + startRow += 1 + } + self.dataArr += dataArr + + CATransaction.setCompletionBlock(nil) + CATransaction.begin() + self.playerListVC?.collectionView.insertItems(at: indexPaths) + CATransaction.commit() + } +} + +extension XSDiscoverViewModel { + + @MainActor + func requestDataArr(page: Int, completer: (() -> Void)? = nil) async { + guard let dataArr = await XSHomeAPI.requestDiscoverData(page: page) else { return } + self.page = page + if page == 1 { + self.playerListVC?.clearData() + self.dataArr = dataArr + self.playerListVC?.reloadData { [weak self] in + + self?.playerListVC?.scrollToItem(indexPath: .init(row: 0, section: 0), animated: false) + } + } else { + self.addDataArr(dataArr: dataArr) + } + + } +} diff --git a/XSeri/Class/Home/Controller/XSHomeCategoriesViewController.swift b/XSeri/Class/Home/Controller/XSHomeCategoriesViewController.swift new file mode 100644 index 0000000..e730fb8 --- /dev/null +++ b/XSeri/Class/Home/Controller/XSHomeCategoriesViewController.swift @@ -0,0 +1,133 @@ +// +// XSHomeCategoriesViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeCategoriesViewController: XSHomeChildViewController { + + + private lazy var categoryArr: [XSCategoryModel] = [] + + private lazy var categoryIndex = 0 + + private lazy var dataArr: [XSShortModel] = [] + + private lazy var page = 1 + + private lazy var layout: UICollectionViewFlowLayout = { + let width = (XSScreen.width - 32 - 20) / 3 + let coverHeight = 142 / 107 * width + let height = coverHeight + 40 + + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: floor(width), height: height) + layout.minimumLineSpacing = 10 + layout.minimumInteritemSpacing = 10 + layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16) + layout.headerReferenceSize = .init(width: XSScreen.width, height: 1) + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0) + collectionView.register(XSHomeCategoriesCell.self, forCellWithReuseIdentifier: "cell") + collectionView.register(XSHomeCategoriesHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "header") + return collectionView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + xs_setupUI() + + Task { + await requestCategories() + await requestCategoryData(page: 1) + } + } + +} + +extension XSHomeCategoriesViewController { + private func xs_setupUI() { + view.addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalToSuperview().offset(self.topHeight + 15) + } + } +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSHomeCategoriesViewController: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSHomeCategoriesCell + cell.model = self.dataArr[indexPath.row] + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! XSHomeCategoriesHeaderView + view.didChangeHeight = { [weak self] height in + guard let self = self else { return } + self.layout.headerReferenceSize = .init(width: XSScreen.width, height: height) + } + view.didSelected = { [weak self] index in + guard let self = self else { return } + self.categoryIndex = index + Task { + await self.requestCategoryData(page: 1) + } + } + view.dataArr = self.categoryArr + return view + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = self.dataArr[indexPath.row] + let vc = XSShortDetailViewController() + vc.shortId = model.short_play_id + self.navigationController?.pushViewController(vc, animated: true) + } +} + +extension XSHomeCategoriesViewController { + + private func requestCategories() async { + guard let dataArr = await XSHomeAPI.requestCategoryList() else { return } + + self.categoryArr = dataArr + self.layout.headerReferenceSize = .init(width: XSScreen.width, height: 1) + self.collectionView.reloadData() + } + + private func requestCategoryData(page: Int) async { + guard self.categoryIndex < self.categoryArr.count else { return } + guard let id = self.categoryArr[categoryIndex].id else { return } + + guard let dataArr = await XSHomeAPI.requestCategoryVideo(id: id, page: page) else { return } + + if page == 1 { + self.dataArr.removeAll() + } + self.dataArr += dataArr + self.page = page + self.collectionView.reloadData() + } + +} diff --git a/XSeri/Class/Home/Controller/XSHomeChildViewController.swift b/XSeri/Class/Home/Controller/XSHomeChildViewController.swift new file mode 100644 index 0000000..6c8174a --- /dev/null +++ b/XSeri/Class/Home/Controller/XSHomeChildViewController.swift @@ -0,0 +1,24 @@ +// +// XSHomeChildViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeChildViewController: XSViewController { + + var topHeight: CGFloat { + return XSScreen.safeTop + 95 + } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .clear + + } + + +} diff --git a/XSeri/Class/Home/Controller/XSHomeNewViewController.swift b/XSeri/Class/Home/Controller/XSHomeNewViewController.swift new file mode 100644 index 0000000..3ad30b3 --- /dev/null +++ b/XSeri/Class/Home/Controller/XSHomeNewViewController.swift @@ -0,0 +1,180 @@ +// +// XSHomeNewViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeNewViewController: XSHomeChildViewController { + + private lazy var dataArr: [XSShortModel] = [] + private lazy var page: Int = 1 + + + private lazy var layout: UICollectionViewCompositionalLayout = { + let layout = UICollectionViewCompositionalLayout { section, environment in + + + if section == 0 { + let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(32)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) + + let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .absolute(210), heightDimension: .absolute(360)), subitems: [item]) + let layoutSection = NSCollectionLayoutSection(group: group) + layoutSection.interGroupSpacing = 15 + layoutSection.orthogonalScrollingBehavior = .continuous + layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16) + layoutSection.boundarySupplementaryItems = [headerItem] + return layoutSection + } else { + let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(57)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) + + let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(146)), subitems: [item]) + + let layoutSection = NSCollectionLayoutSection(group: group) + layoutSection.interGroupSpacing = 12 + layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16) + layoutSection.boundarySupplementaryItems = [headerItem] + return layoutSection + } + } + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0) + collectionView.xs_addRefreshHeader { [weak self] in + self?.handleHeaderRefresh(nil) + } + collectionView.xs_addRefreshFooter(insetBottom: 0) { [weak self] in + self?.handleFooterRefresh(nil) + } + collectionView.register(XSHomeNewBigCell.self, forCellWithReuseIdentifier: "XSHomeNewBigCell") + collectionView.register(XSHomeNewCell.self, forCellWithReuseIdentifier: "XSHomeNewCell") + collectionView.register(XSHomeNewTitleView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "title") + return collectionView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + xs_setupUI() + + Task { + await requestDataArr(page: 1) + } + } + + override func handleHeaderRefresh(_ completer: (() -> Void)?) { + Task { + await requestDataArr(page: 1) + self.collectionView.xs_endHeaderRefreshing() + } + } + + override func handleFooterRefresh(_ completer: (() -> Void)?) { + Task { + await requestDataArr(page: self.page + 1) + self.collectionView.xs_endFooterRefreshing() + } + } + + +} + +extension XSHomeNewViewController { + + private func xs_setupUI() { + view.addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalToSuperview().offset(self.topHeight + 13) + } + + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSHomeNewViewController: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + var model: XSShortModel + if indexPath.section == 0 { + model = self.dataArr[indexPath.row] + } else { + model = self.dataArr[indexPath.row + 3] + } + + + if indexPath.section == 0 { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomeNewBigCell", for: indexPath) as! XSHomeNewBigCell + cell.model = model + return cell + } else { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomeNewCell", for: indexPath) as! XSHomeNewCell + cell.model = model + return cell + } + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + let totalCount = self.dataArr.count + if section == 0 { + return min(3, totalCount) + } else { + return totalCount - 3 + } + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + if self.dataArr.count > 3 { + return 2 + } else if self.dataArr.count > 0 { + return 1 + } else { + return 0 + } + } + + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "title", for: indexPath) as! XSHomeNewTitleView + view.titleLabel.text = "New Releases".localized + return view + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + var model: XSShortModel + if indexPath.section == 0 { + model = self.dataArr[indexPath.row] + } else { + model = self.dataArr[indexPath.row + 3] + } + let vc = XSShortDetailViewController() + vc.shortId = model.short_play_id + self.navigationController?.pushViewController(vc, animated: true) + } +} + +extension XSHomeNewViewController { + + private func requestDataArr(page: Int) async { + guard let list = await XSHomeAPI.requestHomeNew(page: page) else { return } + if page == 1 { + self.dataArr.removeAll() + } + self.page = page + self.dataArr += list + + self.collectionView.reloadData() + } + +} diff --git a/XSeri/Class/Home/Controller/XSHomePopularViewController.swift b/XSeri/Class/Home/Controller/XSHomePopularViewController.swift new file mode 100644 index 0000000..d4ee335 --- /dev/null +++ b/XSeri/Class/Home/Controller/XSHomePopularViewController.swift @@ -0,0 +1,165 @@ +// +// XSHomePopularViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import SnapKit + +class XSHomePopularViewController: XSHomeChildViewController { + + private lazy var viewModel = XSHomeViewModel() + + private lazy var layout: XSWaterfallFlowLayout = { + let layout = XSWaterfallFlowLayout() + layout.delegate = self + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: self.layout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0) + collectionView.xs_addRefreshHeader { [weak self] in + self?.handleHeaderRefresh(nil) + } + collectionView.register(XSHomePopularCell.self, forCellWithReuseIdentifier: "XSHomePopularCell") + collectionView.register(XSHomePopularBigCell.self, forCellWithReuseIdentifier: "XSHomePopularBigCell") + return collectionView + }() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil) + + xs_setupUI() + + Task { + await self.requestDataArr() + } + } + + override func handleHeaderRefresh(_ completer: (() -> Void)?) { + Task { + await self.requestDataArr() + self.collectionView.xs_endHeaderRefreshing() + } + } + + @objc private func networkStatusDidChangeNotification() { + guard XSNetworkMonitorManager.manager.isReachable == true else { return } + guard self.viewModel.dataArr.isEmpty else { return } + Task { + await self.requestDataArr() + } + } + +} + +extension XSHomePopularViewController { + + private func xs_setupUI() { + view.addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalToSuperview().offset(self.topHeight + 15) + } + + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSHomePopularViewController: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let itemModel = self.viewModel.dataArr[indexPath.section] + let model = itemModel.list?[indexPath.row] + + if itemModel.module_key == .week_highest_recommend { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomePopularCell", for: indexPath) as! XSHomePopularCell + cell.model = model + return cell + } else { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSHomePopularBigCell", for: indexPath) as! XSHomePopularBigCell + cell.model = model + return cell + } + + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.viewModel.dataArr[section].list?.count ?? 0 + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return self.viewModel.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let itemModel = self.viewModel.dataArr[indexPath.section] + let model = itemModel.list?[indexPath.row] + let vc = XSShortDetailViewController() + vc.shortId = model?.short_play_id + self.navigationController?.pushViewController(vc, animated: true) + } + +} + +//MARK: XSWaterfallMutiSectionDelegate +extension XSHomePopularViewController: XSWaterfallMutiSectionDelegate { + func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat { + if indexPath.section == 0 { + return 146 + 57 + } else { + return 219 + 92 + } + } + + func columnNumber(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> Int { + if section == 0 { + return 3 + } else { + return 2 + } + } + + func insetForSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> UIEdgeInsets { + return .init(top: 0, left: 16, bottom: 0, right: 16) + } + + func interitemSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat { + return 13 + } + + func lineSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat { + return 16 + } + + func spacingWithLastSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat { + if section > 0 { + return 26 + } else { + return 0 + } + } +} + +extension XSHomePopularViewController { + + private func requestDataArr() async { + await self.viewModel.requestHomeData() + self.collectionView.reloadData() + } + +} + + diff --git a/XSeri/Class/Home/Controller/XSHomeRankingsViewController.swift b/XSeri/Class/Home/Controller/XSHomeRankingsViewController.swift new file mode 100644 index 0000000..017bb70 --- /dev/null +++ b/XSeri/Class/Home/Controller/XSHomeRankingsViewController.swift @@ -0,0 +1,111 @@ +// +// XSHomeRankingsViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeRankingsViewController: XSHomeChildViewController { + + private lazy var dataArr: [XSShortModel] = [] + + private lazy var bgView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "rankings_bg_image")) + return imageView + }() + + private lazy var layout: UICollectionViewLayout = { + let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))) + + let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(84)), subitems: [item]) + + let layoutSection = NSCollectionLayoutSection(group: group) + layoutSection.interGroupSpacing = 10 + layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16) + + let layout = UICollectionViewCompositionalLayout(section: layoutSection) + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 0, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0) + collectionView.xs_addRefreshHeader { [weak self] in + self?.handleHeaderRefresh(nil) + } + collectionView.register(XSHomeRankingsCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + xs_setupUI() + + Task { + await requestDataArr() + } + } + + override func handleHeaderRefresh(_ completer: (() -> Void)?) { + Task { + await requestDataArr() + self.collectionView.xs_endHeaderRefreshing() + } + } +} + +extension XSHomeRankingsViewController { + + private func xs_setupUI() { + view.addSubview(bgView) + view.addSubview(collectionView) + + bgView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + } + + collectionView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalToSuperview().offset(self.topHeight + 15) + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSHomeRankingsViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSHomeRankingsCell + cell.num = indexPath.row + 1 + cell.model = self.dataArr[indexPath.row] + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = self.dataArr[indexPath.row] + let vc = XSShortDetailViewController() + vc.shortId = model.short_play_id + self.navigationController?.pushViewController(vc, animated: true) + } +} + +extension XSHomeRankingsViewController { + + private func requestDataArr() async { + guard let list = await XSHomeAPI.requestHomeRankings() else { return } + + self.dataArr = list + self.collectionView.reloadData() + } + +} diff --git a/XSeri/Class/Home/Controller/XSHomeViewController.swift b/XSeri/Class/Home/Controller/XSHomeViewController.swift new file mode 100644 index 0000000..6828e5e --- /dev/null +++ b/XSeri/Class/Home/Controller/XSHomeViewController.swift @@ -0,0 +1,131 @@ +// +// XSHomeViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import SnapKit +import JXSegmentedView + +class XSHomeViewController: XSViewController { + + private let titles: [String] = [ + "Popular".localized, + "New".localized, + "Rankings".localized, + "Categories".localized, + ] + + private let viewControllers: [XSViewController] = { + let vc1 = XSHomePopularViewController() + let vc2 = XSHomeNewViewController() + let vc3 = XSHomeRankingsViewController() + let vc4 = XSHomeCategoriesViewController() + return [vc1, vc2, vc3, vc4] + }() + + // MARK: - UI Components + private lazy var searchButton: XSHomeSearchButton = { + let button = XSHomeSearchButton() + button.addAction(UIAction(handler: { [weak self] _ in + guard let self = self else { return } + let vc = XSSearchViewController() + self.navigationController?.pushViewController(vc, animated: true) + }), for: .touchUpInside) + return button + }() + + /// Segmented 数据源 + private lazy var segmentedDataSource: JXSegmentedTitleDataSource = { + let dataSource = JXSegmentedTitleDataSource() + dataSource.titles = self.titles + dataSource.titleNormalColor = UIColor.white.withAlphaComponent(0.55) + dataSource.titleSelectedColor = .white + dataSource.titleNormalFont = .font(ofSize: 16, weight: .medium) + dataSource.titleSelectedFont = .font(ofSize: 16, weight: .semibold) + dataSource.isTitleColorGradientEnabled = true + dataSource.itemSpacing = 24 + return dataSource + }() + + /// Segmented 视图 (Tab 栏) + private lazy var segmentedView: JXSegmentedView = { + let view = JXSegmentedView() + view.dataSource = segmentedDataSource + view.delegate = self + view.listContainer = listContainerView + view.contentEdgeInsetLeft = 16 + view.contentEdgeInsetRight = 16 + view.backgroundColor = .clear + return view + }() + + /// 列表容器 (内容展示) + private lazy var listContainerView: JXSegmentedListContainerView = { + return JXSegmentedListContainerView(dataSource: self) + }() + + // MARK: - Life Cycle + override func viewDidLoad() { + super.viewDidLoad() + xs_setupUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } +} + +// MARK: - UI Setup +extension XSHomeViewController { + + private func xs_setupUI() { + view.addSubview(listContainerView) + view.addSubview(searchButton) + view.addSubview(segmentedView) + + searchButton.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.centerX.equalToSuperview() + make.top.equalToSuperview().offset(XSScreen.safeTop + 15) + make.height.equalTo(32) + } + + + segmentedView.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.equalToSuperview() + make.top.equalTo(searchButton.snp.bottom).offset(10) + make.height.equalTo(30) + } + + + listContainerView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalToSuperview() +// make.top.equalTo(segmentedView.snp.bottom).offset(8) + } + + } +} + +// MARK: - JXSegmentedViewDelegate +extension XSHomeViewController: JXSegmentedViewDelegate { + func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { + // Tab 切换回调 + } +} + +// MARK: - JXSegmentedListContainerViewDataSource +extension XSHomeViewController: JXSegmentedListContainerViewDataSource { + func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int { + return segmentedDataSource.titles.count + } + + func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate { + return viewControllers[index] + } +} diff --git a/XSeri/Class/Home/Controller/XSSearchViewController.swift b/XSeri/Class/Home/Controller/XSSearchViewController.swift new file mode 100644 index 0000000..a22a66a --- /dev/null +++ b/XSeri/Class/Home/Controller/XSSearchViewController.swift @@ -0,0 +1,390 @@ +// +// XSSearchViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit +import LYEmptyView + +final class XSSearchViewController: XSViewController { + + private enum Mode { + case idle + case suggest + case result + } + + private let headerView = XSSearchHeaderView() + private let historyHotView = XSSearchHistoryHotView() + + private lazy var suggestionCollectionView: XSCollectionView = { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 12 + layout.minimumInteritemSpacing = 0 + layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16) + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.isScrollEnabled = true + collectionView.keyboardDismissMode = .onDrag + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(XSSearchSuggestionCell.self, forCellWithReuseIdentifier: "XSSearchSuggestionCell") + return collectionView + }() + + private lazy var resultCollectionView: XSCollectionView = { + let layout = UICollectionViewFlowLayout() + layout.minimumLineSpacing = 16 + layout.minimumInteritemSpacing = 8 + layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16) + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.isScrollEnabled = true + collectionView.keyboardDismissMode = .onDrag + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(XSSearchResultCell.self, forCellWithReuseIdentifier: "XSSearchResultCell") + return collectionView + }() + + private var history: [String] = [] + private var suggestionItems: [XSShortModel] = [] + private var resultItems: [XSShortModel] = [] + private var currentSuggestKeyword = "" + private var currentResultKeyword = "" + + private var mode: Mode = .idle { + didSet { + xs_updateVisibility() + } + } + + private var suggestionTask: Task? + private var resultTask: Task? + private var hotTask: Task? + + deinit { + suggestionTask?.cancel() + resultTask?.cancel() + hotTask?.cancel() + } + + override func viewDidLoad() { + super.viewDidLoad() + + xs_setupUI() + xs_bindData() + xs_loadHistory() + xs_loadHotSearches() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + let _ = self.headerView.becomeFirstResponder() + } + +} + +extension XSSearchViewController { + + private func xs_setupUI() { + view.addSubview(headerView) + view.addSubview(historyHotView) + view.addSubview(suggestionCollectionView) + view.addSubview(resultCollectionView) + + headerView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(XSScreen.safeTop + 12) + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(38) + } + + historyHotView.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom).offset(22) + make.left.right.equalToSuperview() + } + + suggestionCollectionView.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom).offset(22) + make.left.right.equalToSuperview() + make.bottom.equalTo(view.safeAreaLayoutGuide) + } + + resultCollectionView.snp.makeConstraints { make in + make.top.equalTo(headerView.snp.bottom).offset(22) + make.left.right.equalToSuperview() + make.bottom.equalTo(view.safeAreaLayoutGuide) + } + + // 搜索结果为空时显示空白样式 + resultCollectionView.ly_emptyView = XSEmpty.xs_emptyView() + // 联想为空时显示空白样式 + suggestionCollectionView.ly_emptyView = XSEmpty.xs_emptyView() + } + + private func xs_bindData() { + headerView.placeholder = XSSearchData.searchPlaceholder + headerView.didTapBack = { [weak self] in + self?.navigationController?.popViewController(animated: true) + } + headerView.didBeginEditing = { [weak self] text in + guard let self = self else { return } + // 已经显示搜索结果时,不在进入编辑时立刻切换联想 + if self.mode == .result { return } + self.xs_handleSuggest(text) + } + headerView.didChangeText = { [weak self] text in + self?.xs_handleSuggest(text) + } + headerView.didTapSearch = { [weak self] text in + self?.xs_submitSearch(text) + } + + historyHotView.historyTitle = XSSearchData.recentTitle + historyHotView.didTapClear = { [weak self] in + self?.xs_clearHistory() + } + historyHotView.didSelectHistory = { [weak self] text in + self?.headerView.text = text + self?.xs_submitSearch(text) + } + historyHotView.didSelectHot = { [weak self] model in + self?.xs_pushDetail(model: model) + } + historyHotView.hotTitle = XSSearchData.hotSearchesTitle + xs_updateVisibility() + } +} + +// MARK: - Data +extension XSSearchViewController { + + private func xs_loadHotSearches() { + hotTask?.cancel() + hotTask = Task { [weak self] in + // 热门榜单数据来源接口 + let hotList = await XSHomeAPI.requestHotSearchList() ?? [] + let topList = await XSHomeAPI.requestTopSearchList() ?? [] + + guard !Task.isCancelled else { return } + await MainActor.run { + self?.xs_updateHotSection(hotList: Array(hotList.prefix(5)), topList: Array(topList.prefix(5))) + } + } + } + + private func xs_updateHotSection(hotList: [XSShortModel], topList: [XSShortModel]) { + + var arr: [XSSearchHotSection] = [] + + if !hotList.isEmpty { + arr.append(XSSearchHotSection(icon: UIImage(named: "hot_icon_03"), title: XSSearchData.hotSectionTitles[0], style: .gold, items: hotList)) + } + if !topList.isEmpty { + arr.append(XSSearchHotSection(icon: UIImage(named: "hot_icon_04"), title: XSSearchData.hotSectionTitles[1], style: .purple, items: topList)) + } + historyHotView.hotSections = arr + + xs_updateVisibility() + } + + private func xs_fetchSuggestions(_ keyword: String) { + suggestionTask?.cancel() + let expectedKeyword = keyword + suggestionTask = Task { [weak self] in + // 联想词同样走搜索接口 + let list = await XSHomeAPI.requestSearch(text: keyword) ?? [] + guard !Task.isCancelled else { return } + await MainActor.run { + guard let self = self, + self.mode == .suggest, + self.currentSuggestKeyword == expectedKeyword else { + return + } + self.suggestionItems = list + self.suggestionCollectionView.reloadData() + } + } + } + + private func xs_fetchResults(_ keyword: String) { + resultTask?.cancel() + let expectedKeyword = keyword + resultTask = Task { [weak self] in + // 搜索结果走搜索接口 + let list = await XSHomeAPI.requestSearch(text: keyword) ?? [] + guard !Task.isCancelled else { return } + await MainActor.run { + guard let self = self, + self.mode == .result, + self.currentResultKeyword == expectedKeyword else { + return + } + self.resultItems = list + self.resultCollectionView.reloadData() + } + } + } +} + +// MARK: - Actions +extension XSSearchViewController { + + private func xs_handleSuggest(_ text: String?) { + let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + if keyword.isEmpty { + suggestionTask?.cancel() + resultTask?.cancel() + currentSuggestKeyword = "" + mode = .idle + suggestionItems = [] + suggestionCollectionView.reloadData() + return + } + + resultTask?.cancel() + resultItems = [] + resultCollectionView.reloadData() + resultCollectionView.isHidden = true + currentSuggestKeyword = keyword + mode = .suggest + suggestionCollectionView.isHidden = false + xs_fetchSuggestions(keyword) + } + + private func xs_submitSearch(_ text: String?) { + let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + guard !keyword.isEmpty else { + mode = .idle + return + } + + suggestionTask?.cancel() + suggestionItems = [] + suggestionCollectionView.reloadData() + suggestionCollectionView.isHidden = true + currentResultKeyword = keyword + view.endEditing(true) + mode = .result + xs_addHistory(keyword) + xs_fetchResults(keyword) + } +} + +// MARK: - History +extension XSSearchViewController { + + private func xs_loadHistory() { + history = UserDefaults.standard.stringArray(forKey: XSSearchData.historyKey) ?? [] + historyHotView.historyTags = history + xs_updateVisibility() + } + + private func xs_addHistory(_ keyword: String) { + // 搜索历史本地缓存,最多保留 10 条 + history.removeAll { $0.caseInsensitiveCompare(keyword) == .orderedSame } + history.insert(keyword, at: 0) + if history.count > 10 { + history = Array(history.prefix(10)) + } + UserDefaults.standard.set(history, forKey: XSSearchData.historyKey) + historyHotView.historyTags = history + xs_updateVisibility() + } + + private func xs_clearHistory() { + history = [] + UserDefaults.standard.removeObject(forKey: XSSearchData.historyKey) + historyHotView.historyTags = [] + xs_updateVisibility() + } +} + +// MARK: - Layout +extension XSSearchViewController { + + private func xs_updateVisibility() { + switch mode { + case .idle: + historyHotView.isHidden = !historyHotView.hasContent + suggestionCollectionView.isHidden = true + resultCollectionView.isHidden = true + headerView.style = .home + case .suggest: + historyHotView.isHidden = true + suggestionCollectionView.isHidden = false + resultCollectionView.isHidden = true + headerView.style = .input + case .result: + historyHotView.isHidden = true + suggestionCollectionView.isHidden = true + resultCollectionView.isHidden = false + headerView.style = .input + } + } +} + +// MARK: - UICollectionViewDelegate UICollectionViewDataSource +extension XSSearchViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + if collectionView == suggestionCollectionView { + return suggestionItems.count + } + return resultItems.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + if collectionView == suggestionCollectionView { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSSearchSuggestionCell", for: indexPath) as! XSSearchSuggestionCell + let item = suggestionItems[indexPath.row] + let keyword = headerView.text ?? "" + cell.configure(model: item, keyword: keyword) + return cell + } else { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "XSSearchResultCell", for: indexPath) as! XSSearchResultCell + cell.configure(model: resultItems[indexPath.row]) + return cell + } + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if collectionView == suggestionCollectionView { + let item = suggestionItems[indexPath.row] + xs_pushDetail(model: item) + } else if collectionView == resultCollectionView { + let item = resultItems[indexPath.row] + xs_pushDetail(model: item) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + if collectionView == suggestionCollectionView { + let width = XSScreen.width - 32 + return CGSize(width: floor(width), height: 104) + } else { + let width = (XSScreen.width - 32 - 16) / 3 + return CGSize(width: floor(width), height: 186) + } + } +} + +// MARK: - Detail +extension XSSearchViewController { + + private func xs_pushDetail(model: XSShortModel) { + let shortId = model.short_play_id ?? model.short_play_video_id ?? model.id + guard let shortId = shortId, !shortId.isEmpty else { return } + let controller = XSShortDetailViewController() + controller.shortId = shortId + navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/XSeri/Class/Home/Model/XSCategoryModel.swift b/XSeri/Class/Home/Model/XSCategoryModel.swift new file mode 100644 index 0000000..0e44eed --- /dev/null +++ b/XSeri/Class/Home/Model/XSCategoryModel.swift @@ -0,0 +1,22 @@ +// +// XSCategoryModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import SmartCodable + +struct XSCategoryModel: SmartCodable { + + var id: String? + var name: String? + + static func mappingForKey() -> [SmartKeyTransformer]? { + return [ + CodingKeys.id <--- ["id", "category_id"], + CodingKeys.name <--- ["name", "category_name"] + ] + } +} diff --git a/XSeri/Class/Home/Model/XSHomeData.swift b/XSeri/Class/Home/Model/XSHomeData.swift new file mode 100644 index 0000000..746a9a6 --- /dev/null +++ b/XSeri/Class/Home/Model/XSHomeData.swift @@ -0,0 +1,16 @@ +import Foundation + +struct XSHomeData { + + /// 首页顶部 Tab 分类 + static let topTabs = [ + "Popular", + "New", + "Rankings", + "Categories", + "aaaaa", + "gfgfggg", + "ffffff" + ] + +} diff --git a/XSeri/Class/Home/Model/XSHomeModuleItem.swift b/XSeri/Class/Home/Model/XSHomeModuleItem.swift new file mode 100644 index 0000000..2c9da2f --- /dev/null +++ b/XSeri/Class/Home/Model/XSHomeModuleItem.swift @@ -0,0 +1,66 @@ +// +// XSHomeModuleItem.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import SmartCodable + +class XSHomeModuleItem: NSObject, SmartCodable { + + enum ModuleKey: String, SmartCaseDefaultable { + case banner = "home_banner" + case v3_recommand = "home_v3_recommand" + ///分类推荐 + case cagetory_recommand = "home_cagetory_recommand" + case week_ranking = "week_ranking" + ///跑马灯 + case marquee = "marquee" + + case new_recommand = "new_recommand" + + case week_highest_recommend = "week_highest_recommend" + } + + required override init() { } + + var module_key: ModuleKey? + var title: String? + var list: [XSShortModel]? + + @SmartAny + var data: Any? + + @IgnoredKey + var iconImage: UIImage? + + @IgnoredKey + var br_cellHeight: CGFloat? + + + + func didFinishMapping() { + if let data = data as? [[String : Any]] { + self.list = [XSShortModel].deserialize(from: data) + } else if let data = data as? [String : Any] { + var dataList: [[String : Any]]? + if let list = data["list"] as? [[String : Any]] { + self.title = data["title"] as? String + dataList = list + + } else if let list = data["shortPlayList"] as? [[String : Any]] { + self.title = data["category_name"] as? String + dataList = list + } + + if let dataList = dataList { + self.list = [XSShortModel].deserialize(from: dataList) + } + + } + } + + +} diff --git a/XSeri/Class/Home/Model/XSSearchData.swift b/XSeri/Class/Home/Model/XSSearchData.swift new file mode 100644 index 0000000..80b693b --- /dev/null +++ b/XSeri/Class/Home/Model/XSSearchData.swift @@ -0,0 +1,41 @@ +// +// XSSearchData.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import Foundation +import UIKit + +/// 搜索页静态数据,集中管理文字与列表,避免视图中硬编码 +struct XSSearchData { + + /// 搜索框占位文案 + static let searchPlaceholder = "Love in Ashes is he".localized + /// 最近搜索标题 + static let recentTitle = "Recent".localized + /// 热门搜索标题 + static let hotSearchesTitle = "Hot Searches".localized + /// 热门榜单子标题 + static let hotSectionTitles = [ + "Rising Popularity".localized, + "Top-Rated Charts".localized + ] + /// 搜索历史存储键 + static let historyKey = "xs_search_history" +} + +/// 榜单板块模型 +struct XSSearchHotSection { + let icon: UIImage? + let title: String + let style: XSSearchHotSectionStyle + let items: [XSShortModel] +} + +/// 榜单样式类型 +enum XSSearchHotSectionStyle { + case gold + case purple +} diff --git a/XSeri/Class/Home/View/XSHomeCategoriesCell.swift b/XSeri/Class/Home/View/XSHomeCategoriesCell.swift new file mode 100644 index 0000000..903f171 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeCategoriesCell.swift @@ -0,0 +1,69 @@ +// +// XSHomeCategoriesCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeCategoriesCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 10 + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .semibold) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.layer.cornerRadius = 10 + contentView.layer.masksToBounds = true + contentView.layer.borderWidth = 1 + contentView.layer.borderColor = UIColor._6_D_71_E_0.withAlphaComponent(0.4).cgColor + + xs_layoutUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSHomeCategoriesCell { + + private func xs_layoutUI() { + contentView.addSubview(coverImageView) + contentView.addSubview(titleLabel) + + coverImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview().offset(-40) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(5) + make.right.lessThanOrEqualToSuperview().offset(-5) + make.top.equalTo(coverImageView.snp.bottom).offset(6) + } + } + +} diff --git a/XSeri/Class/Home/View/XSHomeCategoriesHeaderView.swift b/XSeri/Class/Home/View/XSHomeCategoriesHeaderView.swift new file mode 100644 index 0000000..cc52ffb --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeCategoriesHeaderView.swift @@ -0,0 +1,121 @@ +// +// XSHomeCategoriesHeaderView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import collection_view_layouts +import SnapKit + +class XSHomeCategoriesHeaderView: UICollectionReusableView { + + + var didChangeHeight: ((_ height: CGFloat) -> Void)? + + var didSelected: ((_ index: Int) -> Void)? + + var dataArr: [XSCategoryModel] = [] { + didSet { + self.collectionView.reloadData() + } + } + + var currentIndex: Int = 0 { + didSet { + self.collectionView.reloadData() + } + } + + private lazy var layout: TagsLayout = { + let layout = TagsLayout() + layout.delegate = self + layout.cellsPadding = .init(horizontal: 12, vertical: 16) + layout.contentPadding = .init(horizontal: 16, vertical: 0) + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.isScrollEnabled = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.addObserver(self, forKeyPath: "contentSize", context: nil) + collectionView.register(XSHomeCategoriesTagsCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + deinit { + self.collectionView.removeObserver(self, forKeyPath: "contentSize") + } + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "contentSize" { + self.didChangeHeight?(self.collectionView.contentSize.height + 14) + } + } + +} + +extension XSHomeCategoriesHeaderView { + + private func xs_setupUI() { + addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalToSuperview() + make.bottom.equalToSuperview().offset(-14) + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSHomeCategoriesHeaderView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSHomeCategoriesTagsCell + cell.xs_isSelected = currentIndex == indexPath.row + cell.model = self.dataArr[indexPath.row] + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard currentIndex != indexPath.row else { return } + self.currentIndex = indexPath.row + self.collectionView.reloadData() + + self.didSelected?(indexPath.row) + + } + +} + +//MARK: LayoutDelegate +extension XSHomeCategoriesHeaderView: LayoutDelegate { + + func cellSize(indexPath: IndexPath) -> CGSize { + let model = self.dataArr[indexPath.row] + let text = model.name ?? "" + let width = text.size(XSHomeCategoriesTagsCell.textFont, .init(width: XSScreen.width, height: 27)).width + + return .init(width: width + 24, height: 27) + } + + +} diff --git a/XSeri/Class/Home/View/XSHomeCategoriesTagsCell.swift b/XSeri/Class/Home/View/XSHomeCategoriesTagsCell.swift new file mode 100644 index 0000000..63378a6 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeCategoriesTagsCell.swift @@ -0,0 +1,81 @@ +// +// XSHomeCategoriesTagsCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import SnapKit + +class XSHomeCategoriesTagsCell: UICollectionViewCell { + static let textFont = UIFont.font(ofSize: 12, weight: .regular) + static let selectedTextFont = UIFont.font(ofSize: 12, weight: .bold) + static let textColor = UIColor.white + static let selectedTextColor = UIColor._282828 + + + var model: XSCategoryModel? { + didSet { + titleLabel.text = model?.name + } + } + + var xs_isSelected: Bool = false { + didSet { + if xs_isSelected { + titleLabel.font = Self.selectedTextFont + titleLabel.textColor = Self.selectedTextColor + bgView.isHidden = false + } else { + titleLabel.font = Self.textFont + titleLabel.textColor = Self.textColor + bgView.isHidden = true + } + } + } + + private lazy var bgView: UIImageView = { + let view = UIImageView(image: UIImage(named: "gradient_color_image_01")) + return view + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + self.contentView.backgroundColor = .FFEFD_8.withAlphaComponent(0.15) + self.contentView.layer.masksToBounds = true + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + self.contentView.layer.cornerRadius = self.contentView.bounds.height / 2 + } +} + +extension XSHomeCategoriesTagsCell { + + private func xs_setupUI() { + contentView.addSubview(bgView) + contentView.addSubview(titleLabel) + + bgView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + +} diff --git a/XSeri/Class/Home/View/XSHomeNewBigCell.swift b/XSeri/Class/Home/View/XSHomeNewBigCell.swift new file mode 100644 index 0000000..0326bc8 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeNewBigCell.swift @@ -0,0 +1,103 @@ +// +// XSHomeNewBigCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeNewBigCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverBigImageView.xs_setImage(model?.image_url) + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + categoryLabel.text = model?.category?.first + } + } + + private lazy var coverBigImageView: XSImageView = { + let imageView = XSImageView() + return imageView + }() + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 4 + imageView.layer.borderWidth = 1 + imageView.layer.borderColor = UIColor.white.withAlphaComponent(0.25).cgColor + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .medium) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .white.withAlphaComponent(0.5) + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.titleLabel.text = "What Goes Around Comes Around" + self.categoryLabel.text = "Antiquity" + + contentView.backgroundColor = .white.withAlphaComponent(0.06) + contentView.layer.cornerRadius = 8 + contentView.layer.masksToBounds = true + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSHomeNewBigCell { + + private func xs_setupUI() { + contentView.addSubview(coverBigImageView) + contentView.addSubview(coverImageView) + contentView.addSubview(titleLabel) + contentView.addSubview(categoryLabel) + + coverBigImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.height.equalTo(280) + } + + coverImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(9) + make.top.equalTo(coverBigImageView.snp.bottom).offset(9) + make.width.equalTo(45) + make.height.equalTo(60) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(7) + make.right.lessThanOrEqualToSuperview().offset(-10) + make.top.equalTo(coverBigImageView.snp.bottom).offset(9) + } + + categoryLabel.snp.makeConstraints { make in + make.left.equalTo(titleLabel) + make.right.lessThanOrEqualToSuperview().offset(-10) + make.top.equalTo(titleLabel.snp.bottom).offset(3) + } + + } + +} diff --git a/XSeri/Class/Home/View/XSHomeNewCell.swift b/XSeri/Class/Home/View/XSHomeNewCell.swift new file mode 100644 index 0000000..af82a23 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeNewCell.swift @@ -0,0 +1,154 @@ +// +// XSHomeNewCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeNewCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + desLabel.text = model?.xs_description + epLabel.text = "## Episodes".localizedReplace(text: "\(model?.episode_total ?? 0)") + + if let category = model?.category?.first, !category.isEmpty { + categoryView.isHidden = false + categoryLabel.text = category + } else { + categoryView.isHidden = true + } + + if model?.tag_type == .hot { + tagView.isHidden = false + tagView.image = UIImage(named: "hot_icon_02") + } else if model?.tag_type == .new { + tagView.isHidden = false + tagView.image = UIImage(named: "new_icon_01") + } else { + tagView.isHidden = true + } + + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 4 + return imageView + }() + + private lazy var tagView: UIImageView = { + let imageView = UIImageView() + imageView.xs_setCornerRadius(topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 4) + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 16, weight: .medium) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + + private lazy var epLabel: UILabel = { + let label = XSLabel() + label.font = .font(ofSize: 14, weight: .regular) + label.colorImage = UIImage(named: "gradient_color_image_01") + return label + }() + + private lazy var desLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .regular) + label.textColor = .white.withAlphaComponent(0.8) + label.numberOfLines = 2 + return label + }() + + private lazy var categoryView: UIView = { + let view = UIView() + view.layer.cornerRadius = 2 + view.layer.masksToBounds = true + view.backgroundColor = .D_9_D_9_D_9.withAlphaComponent(0.16) + return view + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + titleLabel.text = "The Year You Were Gone fro...fdsafdafdafdsfsafdsafdasfasdfsa" + epLabel.text = "55 Episodes" + desLabel.text = "Maddie, a maid, seeks justice for her murdered father. She u...fdsafdasfsfdads" + categoryLabel.text = "Angst Romance" + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSHomeNewCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + coverImageView.addSubview(tagView) + contentView.addSubview(titleLabel) + contentView.addSubview(epLabel) + contentView.addSubview(desLabel) + contentView.addSubview(categoryView) + categoryView.addSubview(categoryLabel) + + coverImageView.snp.makeConstraints { make in + make.left.top.bottom.equalToSuperview() + make.width.equalTo(105) + } + + tagView.snp.makeConstraints { make in + make.left.top.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(12) + make.right.lessThanOrEqualToSuperview() + make.top.equalToSuperview().offset(5) + } + + epLabel.snp.makeConstraints { make in + make.left.equalTo(titleLabel) + make.top.equalTo(titleLabel.snp.bottom).offset(6) + } + + desLabel.snp.makeConstraints { make in + make.left.equalTo(titleLabel) + make.top.equalTo(epLabel.snp.bottom).offset(6) + make.right.lessThanOrEqualToSuperview() + } + + categoryView.snp.makeConstraints { make in + make.left.equalTo(titleLabel) + make.top.equalTo(desLabel.snp.bottom).offset(6) + make.height.equalTo(18) + } + + categoryLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + make.left.equalToSuperview().offset(6) + } + } + +} diff --git a/XSeri/Class/Home/View/XSHomeNewTitleView.swift b/XSeri/Class/Home/View/XSHomeNewTitleView.swift new file mode 100644 index 0000000..6545853 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeNewTitleView.swift @@ -0,0 +1,34 @@ +// +// XSHomeNewTitleView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeNewTitleView: UICollectionReusableView { + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .FFDAA_4 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(titleLabel) + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview() + make.bottom.equalToSuperview().offset(-14) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/XSeri/Class/Home/View/XSHomePopularBigCell.swift b/XSeri/Class/Home/View/XSHomePopularBigCell.swift new file mode 100644 index 0000000..3482a8a --- /dev/null +++ b/XSeri/Class/Home/View/XSHomePopularBigCell.swift @@ -0,0 +1,139 @@ +// +// XSHomePopularBigCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import SnapKit + +class XSHomePopularBigCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + desLabel.text = model?.xs_description + + if let category = model?.category?.first, !category.isEmpty { + categoryLabel.text = category + categoryBgView.isHidden = false + } else { + categoryBgView.isHidden = true + } + + if model?.tag_type == .hot { + tagView.isHidden = false + tagView.image = UIImage(named: "hot_icon_02") + } else if model?.tag_type == .new { + tagView.isHidden = false + tagView.image = UIImage(named: "new_icon_01") + } else { + tagView.isHidden = true + } + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.masksToBounds = true + imageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + imageView.layer.cornerRadius = 4 + return imageView + }() + + private lazy var tagView: UIImageView = { + let imageView = UIImageView() + imageView.xs_setCornerRadius(topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 4) + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .white + return label + }() + + private lazy var desLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .white.withAlphaComponent(0.5) + label.numberOfLines = 2 + return label + }() + + private lazy var categoryBgView: UIView = { + let view = UIView() + view.layer.cornerRadius = 12 + view.layer.masksToBounds = true + view.layer.borderWidth = 1 + view.layer.borderColor = UIColor.white.withAlphaComponent(0.25).cgColor + return view + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSHomePopularBigCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + coverImageView.addSubview(tagView) + contentView.addSubview(titleLabel) + contentView.addSubview(desLabel) + contentView.addSubview(categoryBgView) + categoryBgView.addSubview(categoryLabel) + + coverImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview().offset(-92) + } + + tagView.snp.makeConstraints { make in + make.left.top.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.lessThanOrEqualToSuperview() + make.top.equalTo(coverImageView.snp.bottom).offset(8) + } + + desLabel.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.lessThanOrEqualToSuperview() + make.top.equalTo(titleLabel.snp.bottom).offset(4) + } + + categoryBgView.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.lessThanOrEqualToSuperview() + make.bottom.equalToSuperview() + make.height.equalTo(24) + } + + categoryLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + make.left.equalToSuperview().offset(8) + } + } + +} diff --git a/XSeri/Class/Home/View/XSHomePopularCell.swift b/XSeri/Class/Home/View/XSHomePopularCell.swift new file mode 100644 index 0000000..0021df0 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomePopularCell.swift @@ -0,0 +1,101 @@ +// +// XSHomePopularCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import SnapKit + +class XSHomePopularCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + categoryLabel.text = model?.category?.first + + if model?.tag_type == .hot { + tagView.isHidden = false + tagView.image = UIImage(named: "hot_icon_02") + } else if model?.tag_type == .new { + tagView.isHidden = false + tagView.image = UIImage(named: "new_icon_01") + } else { + tagView.isHidden = true + } + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 4 + return imageView + }() + + private lazy var tagView: UIImageView = { + let imageView = UIImageView() + imageView.xs_setCornerRadius(topLeft: 0, topRight: 0, bottomLeft: 4, bottomRight: 0) + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .white.withAlphaComponent(0.5) + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + categoryLabel.text = "Revenge" + titleLabel.text = "The Year You Were Gone frfdsafhdaskhfkashfkdsahfdask" + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSHomePopularCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + coverImageView.addSubview(tagView) + contentView.addSubview(titleLabel) + contentView.addSubview(categoryLabel) + + coverImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview().offset(-57) + } + + tagView.snp.makeConstraints { make in + make.right.top.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview() + make.top.equalTo(coverImageView.snp.bottom).offset(5) + make.right.lessThanOrEqualToSuperview() + } + + categoryLabel.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.lessThanOrEqualToSuperview() + make.bottom.equalToSuperview() + } + } + +} diff --git a/XSeri/Class/Home/View/XSHomeRankingsCell.swift b/XSeri/Class/Home/View/XSHomeRankingsCell.swift new file mode 100644 index 0000000..2e0e047 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeRankingsCell.swift @@ -0,0 +1,182 @@ +// +// XSHomeRankingsCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/31. +// + +import UIKit +import SnapKit + +class XSHomeRankingsCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + + if let category = model?.category?.first, !category.isEmpty { + categoryLabel.text = category + categoryView.isHidden = false + } else { + categoryView.isHidden = true + } + + hotView.setNeedsUpdateConfiguration() + } + } + + + var num: Int = 0 { + didSet { + numLabel.text = "\(num)" + + switch num { + case 1: + numBgView.image = UIImage(named: "rankings_num_bg_01") + numLabel.textColor = ._783902 + + case 2: + numBgView.image = UIImage(named: "rankings_num_bg_02") + numLabel.textColor = .white + + default: + numBgView.image = UIImage(named: "rankings_num_bg_03") + numLabel.textColor = .white + } + + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 3 + return imageView + }() + + private lazy var numBgView: UIImageView = { + let imageView = UIImageView() + return imageView + }() + + private lazy var numLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 11, weight: .bold) + return label + }() + + private lazy var hotView: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.contentInsets = .zero + configuration.image = UIImage(named: "hot_icon_01") + configuration.imagePadding = 4 + configuration.imagePlacement = .leading + + let button = UIButton(configuration: configuration) + button.isUserInteractionEnabled = false + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + + let string = NSNumber(value: self.model?.watch_total ?? 0).format() + + button.configuration?.attributedTitle = AttributedString(string, attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 14, weight: .regular), + .foregroundColor : UIColor.white + ])) + } + button.setContentHuggingPriority(.required, for: .horizontal) + button.setContentCompressionResistancePriority(.required, for: .horizontal) + return button + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .medium) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + + private lazy var categoryView: UIView = { + let view = UIView() + view.backgroundColor = .white.withAlphaComponent(0.08) + view.layer.cornerRadius = 2 + view.layer.masksToBounds = true + return view + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.backgroundColor = .white.withAlphaComponent(0.1) + contentView.layer.cornerRadius = 6 + contentView.layer.masksToBounds = true + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSHomeRankingsCell { + + private func xs_setupUI() { + + contentView.addSubview(coverImageView) + coverImageView.addSubview(numBgView) + numBgView.addSubview(numLabel) + contentView.addSubview(hotView) + contentView.addSubview(titleLabel) + contentView.addSubview(categoryView) + categoryView.addSubview(categoryLabel) + + coverImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(12) + make.width.equalTo(45) + make.height.equalTo(60) + } + + numBgView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(-3) + make.left.equalToSuperview().offset(-3) + } + + numLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview().offset(-2) + make.centerY.equalToSuperview().offset(-1) + } + + hotView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-12) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(19) + make.right.lessThanOrEqualTo(hotView.snp.left).offset(-15) + make.top.equalToSuperview().offset(12) + } + + categoryView.snp.makeConstraints { make in + make.left.equalTo(titleLabel) + make.right.lessThanOrEqualTo(hotView.snp.left).offset(-15) + make.top.equalTo(titleLabel.snp.bottom).offset(10) + make.height.equalTo(18) + } + + categoryLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + make.left.equalToSuperview().offset(6) + } + } + +} diff --git a/XSeri/Class/Home/View/XSHomeSearchButton.swift b/XSeri/Class/Home/View/XSHomeSearchButton.swift new file mode 100644 index 0000000..849e2b1 --- /dev/null +++ b/XSeri/Class/Home/View/XSHomeSearchButton.swift @@ -0,0 +1,58 @@ +// +// XSHomeSearchButton.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit +import SnapKit + +class XSHomeSearchButton: UIControl { + + override var intrinsicContentSize: CGSize { + return .init(width: XSScreen.width, height: 32) + } + + private lazy var iconImageView = UIImageView(image: UIImage(named: "search_icon_01")) + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .medium) + label.textColor = .white.withAlphaComponent(0.6) + label.text = "Through the Storm".localized + return label + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white.withAlphaComponent(0.1) + layer.cornerRadius = 8 + layer.masksToBounds = true + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSHomeSearchButton { + private func xs_setupUI() { + addSubview(iconImageView) + addSubview(titleLabel) + + iconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(12) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(iconImageView.snp.right).offset(8) + make.right.lessThanOrEqualToSuperview().offset(-12) + } + } +} diff --git a/XSeri/Class/Home/View/XSSearchGradientButton.swift b/XSeri/Class/Home/View/XSSearchGradientButton.swift new file mode 100644 index 0000000..c9bc689 --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchGradientButton.swift @@ -0,0 +1,58 @@ +// +// XSSearchGradientButton.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +/// 搜索按钮(渐变背景) +final class XSSearchGradientButton: UIControl { + + private let gradientLayer = CAGradientLayer() + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "search_icon_02")) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer.frame = bounds + layer.cornerRadius = bounds.height / 2 + } +} + +extension XSSearchGradientButton { + + private func xs_setupUI() { + // 渐变用于模拟 Figma 中的金色按钮 + gradientLayer.colors = [ + UIColor.F_5_BD_7_E.cgColor, + UIColor.FFEABC.cgColor, + UIColor.FFCF_99.cgColor + ] + gradientLayer.startPoint = CGPoint(x: 0, y: 0.5) + gradientLayer.endPoint = CGPoint(x: 1, y: 0.5) + layer.insertSublayer(gradientLayer, at: 0) + layer.masksToBounds = true + + addSubview(iconImageView) + + iconImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} diff --git a/XSeri/Class/Home/View/XSSearchHeaderView.swift b/XSeri/Class/Home/View/XSSearchHeaderView.swift new file mode 100644 index 0000000..ee3f89b --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchHeaderView.swift @@ -0,0 +1,231 @@ +// +// XSSearchHeaderView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +final class XSSearchHeaderView: UIView { + + enum Style { + case home + case input + } + + var text: String? { + get { textField.text } + set { + textField.text = newValue + xs_updateSearchButtonVisibility(newValue) + } + } + + /// 返回按钮回调 + var didTapBack: (() -> Void)? + /// 搜索按钮回调 + var didTapSearch: ((_ keyword: String?) -> Void)? + /// 输入开始回调 + var didBeginEditing: ((_ keyword: String?) -> Void)? + /// 输入变化回调 + var didChangeText: ((_ keyword: String?) -> Void)? + + var style: Style = .home { + didSet { + xs_applyStyle() + } + } + + var placeholder: String? { + didSet { + // 使用富文本占位,控制颜色与透明度 + let text = placeholder ?? "" + textField.attributedPlaceholder = NSAttributedString( + string: text, + attributes: [ + .foregroundColor: UIColor.white.withAlphaComponent(0.6), + .font: UIFont.font(ofSize: 14, weight: .medium) + ] + ) + } + } + + private lazy var backButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(named: "arrow_left_icon_02"), for: .normal) + button.tintColor = .white + button.addTarget(self, action: #selector(xs_handleBack), for: .touchUpInside) + return button + }() + + private lazy var searchContainer: UIView = { + let view = UIView() + view.backgroundColor = UIColor.white.withAlphaComponent(0.18) + view.layer.cornerRadius = 19 + view.layer.masksToBounds = true + return view + }() + + private lazy var searchIconView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "search_icon_01")) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var textField: UITextField = { + let textField = UITextField() + textField.textColor = .white + textField.font = .font(ofSize: 14, weight: .medium) + textField.clearButtonMode = .never + textField.returnKeyType = .search + textField.delegate = self + textField.addTarget(self, action: #selector(xs_textChanged), for: .editingChanged) + return textField + }() + + private lazy var searchButton = XSSearchGradientButton() + private var containerHeightConstraint: Constraint? + private var textLeadingToSuperview: Constraint? + private var textLeadingToIcon: Constraint? + private var textTrailingToButton: Constraint? + private var textTrailingToSuperview: Constraint? + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func becomeFirstResponder() -> Bool { + let result = super.becomeFirstResponder() + self.textField.becomeFirstResponder() + return result + } +} + +extension XSSearchHeaderView { + + private func xs_setupUI() { + addSubview(backButton) + addSubview(searchContainer) + searchContainer.addSubview(searchIconView) + searchContainer.addSubview(textField) + searchContainer.addSubview(searchButton) + + searchButton.addTarget(self, action: #selector(xs_handleSearch), for: .touchUpInside) + + backButton.snp.makeConstraints { make in + make.left.equalToSuperview() + make.centerY.equalToSuperview() + make.size.equalTo(CGSize(width: 24, height: 24)) + } + + searchContainer.snp.makeConstraints { make in + make.left.equalTo(backButton.snp.right).offset(8) + make.right.equalToSuperview() + make.centerY.equalToSuperview() + containerHeightConstraint = make.height.equalTo(38).constraint + } + + searchIconView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(12) + make.centerY.equalToSuperview() + make.size.equalTo(CGSize(width: 16, height: 16)) + } + + searchButton.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-4) + make.centerY.equalToSuperview() + make.size.equalTo(CGSize(width: 57, height: 30)) + } + + textField.snp.makeConstraints { make in + textLeadingToSuperview = make.left.equalToSuperview().offset(20).constraint + textLeadingToIcon = make.left.equalTo(searchIconView.snp.right).offset(8).constraint + make.centerY.equalToSuperview() + textTrailingToButton = make.right.equalTo(searchButton.snp.left).offset(-8).constraint + textTrailingToSuperview = make.right.equalToSuperview().offset(-12).constraint + make.height.equalTo(30) + } + + xs_applyStyle() + } +} + +extension XSSearchHeaderView { + + @objc private func xs_handleBack() { + didTapBack?() + } + + @objc private func xs_handleSearch() { + didTapSearch?(textField.text) + } + + @objc private func xs_textChanged() { + let value = textField.text + xs_updateSearchButtonVisibility(value) + didChangeText?(value) + } +} + +extension XSSearchHeaderView: UITextFieldDelegate { + + func textFieldDidBeginEditing(_ textField: UITextField) { + didBeginEditing?(textField.text) + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + didTapSearch?(textField.text) + textField.resignFirstResponder() + return true + } +} + +extension XSSearchHeaderView { + + private func xs_applyStyle() { + switch style { + case .home: + searchIconView.isHidden = true + searchContainer.backgroundColor = UIColor.white.withAlphaComponent(0.18) + searchContainer.layer.cornerRadius = 19 + containerHeightConstraint?.update(offset: 38) + textLeadingToSuperview?.activate() + textLeadingToIcon?.deactivate() + case .input: + searchIconView.isHidden = false + searchContainer.backgroundColor = UIColor.white.withAlphaComponent(0.1) + searchContainer.layer.cornerRadius = 18 + containerHeightConstraint?.update(offset: 36) + textLeadingToSuperview?.deactivate() + textLeadingToIcon?.activate() + } + xs_updateSearchButtonVisibility(textField.text) + } + + private func xs_updateSearchButtonVisibility(_ text: String?) { + let keyword = (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let shouldShow: Bool + switch style { + case .home: + shouldShow = true + case .input: + shouldShow = !keyword.isEmpty + } + searchButton.isHidden = !shouldShow + if shouldShow { + textTrailingToSuperview?.deactivate() + textTrailingToButton?.activate() + } else { + textTrailingToButton?.deactivate() + textTrailingToSuperview?.activate() + } + layoutIfNeeded() + } +} diff --git a/XSeri/Class/Home/View/XSSearchHistoryHotView.swift b/XSeri/Class/Home/View/XSSearchHistoryHotView.swift new file mode 100644 index 0000000..261743f --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchHistoryHotView.swift @@ -0,0 +1,110 @@ +// +// XSSearchHistoryHotView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +/// 搜索历史 + 热门榜单容器视图 +final class XSSearchHistoryHotView: UIView { + + /// 是否有可展示内容 + var hasContent: Bool { + return !historyTags.isEmpty || !hotSections.isEmpty + } + + /// 清空历史回调 + var didTapClear: (() -> Void)? { + didSet { + recentView.didTapClear = didTapClear + } + } + /// 点击历史标签回调 + var didSelectHistory: ((_ text: String) -> Void)? { + didSet { + recentView.didSelectTag = didSelectHistory + } + } + + var historyTitle: String? { + didSet { + recentView.title = historyTitle + } + } + + var historyTags: [String] = [] { + didSet { + recentView.tags = historyTags + xs_updateVisibility() + } + } + + var hotTitle: String? { + didSet { + hotSectionView.title = hotTitle + } + } + + /// 点击热门榜单条目回调 + var didSelectHot: ((_ model: XSShortModel) -> Void)? { + didSet { + hotSectionView.didSelectItem = didSelectHot + } + } + + var hotSections: [XSSearchHotSection] = [] { + didSet { + hotSectionView.sections = hotSections + xs_updateVisibility() + } + } + + private let recentContainer = UIView() + private let recentView = XSSearchRecentView() + private let hotSectionView = XSSearchHotSectionView() + + private let stackView: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.spacing = 24 + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSSearchHistoryHotView { + + private func xs_setupUI() { + addSubview(stackView) + recentContainer.addSubview(recentView) + + stackView.addArrangedSubview(recentContainer) + stackView.addArrangedSubview(hotSectionView) + + stackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + recentView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)) + } + + xs_updateVisibility() + } + + private func xs_updateVisibility() { + recentContainer.isHidden = historyTags.isEmpty + hotSectionView.isHidden = hotSections.isEmpty + } +} diff --git a/XSeri/Class/Home/View/XSSearchHotListCardView.swift b/XSeri/Class/Home/View/XSSearchHotListCardView.swift new file mode 100644 index 0000000..70e2459 --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchHotListCardView.swift @@ -0,0 +1,108 @@ +// +// XSSearchHotListCardView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +final class XSSearchHotListCardView: UIView { + + /// 榜单卡片数据 + var section: XSSearchHotSection? { + didSet { + xs_applySection() + } + } + + /// 点击榜单条目回调 + var didSelectItem: ((_ model: XSShortModel) -> Void)? + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + return label + }() + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var stackView: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.spacing = 8 + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSSearchHotListCardView { + + private func xs_setupUI() { + layer.cornerRadius = 12 + layer.masksToBounds = true + layer.borderWidth = 1 + + addSubview(titleLabel) + addSubview(iconImageView) + addSubview(stackView) + + iconImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.top.equalToSuperview().offset(20) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalTo(iconImageView) + make.left.equalTo(iconImageView.snp.right).offset(2) + make.right.lessThanOrEqualToSuperview().offset(-10) + } + + stackView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.top.equalToSuperview().offset(53) + } + } + + private func xs_applySection() { + guard let section = section else { return } + titleLabel.text = section.title + iconImageView.image = section.icon + + switch section.style { + case .gold: + layer.borderColor = UIColor.FDE_7_B_8.withAlphaComponent(0.4).cgColor + titleLabel.textColor = .FDE_7_B_8 + case .purple: + layer.borderColor = UIColor.E_0_C_6_FF.withAlphaComponent(0.2).cgColor + titleLabel.textColor = UIColor.E_0_C_6_FF.withAlphaComponent(0.7) + } + + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + section.items.enumerated().forEach { index, item in + let itemView = XSSearchHotListItemView() + itemView.configure(model: item, rank: index + 1, style: section.style) + itemView.didTap = { [weak self] model in + self?.didSelectItem?(model) + } + itemView.snp.makeConstraints { make in + make.height.equalTo(65) + } + stackView.addArrangedSubview(itemView) + } + } +} diff --git a/XSeri/Class/Home/View/XSSearchHotListItemView.swift b/XSeri/Class/Home/View/XSSearchHotListItemView.swift new file mode 100644 index 0000000..423d065 --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchHotListItemView.swift @@ -0,0 +1,147 @@ +// +// XSSearchHotListItemView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +final class XSSearchHotListItemView: UIView { + + /// 点击条目回调 + var didTap: ((_ model: XSShortModel) -> Void)? + + private var model: XSShortModel? + + private lazy var rankBadgeView: XSView = { + let view = XSView() + view.xs_startPoint = .init(x: 0.5, y: 0) + view.xs_endPoint = .init(x: 0.5, y: 1) + view.layer.masksToBounds = true + return view + }() + + private lazy var rankLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .bold) + return label + }() + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 4 + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .semibold) + label.textColor = UIColor.white.withAlphaComponent(0.9) + label.numberOfLines = 2 + return label + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .medium) + label.textColor = UIColor.white.withAlphaComponent(0.6) + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + rankBadgeView.layer.cornerRadius = rankBadgeView.bounds.height / 2 + } + + func configure(model: XSShortModel, rank: Int, style: XSSearchHotSectionStyle) { + // 根据榜单样式调整数字徽标的颜色 + self.model = model + rankLabel.text = "\(rank)" + titleLabel.text = model.name ?? model.xs_description + let category = model.category?.first ?? model.categoryList?.first?.name ?? "" + categoryLabel.text = category + categoryLabel.isHidden = category.isEmpty + coverImageView.xs_setImage(model.image_url) + + switch style { + case .gold: + if rank <= 3 { + rankBadgeView.xs_colors = [UIColor.FFE_6_B_3.cgColor, UIColor.F_4_C_783.cgColor] + rankLabel.textColor = ._060606 + } else { + rankBadgeView.xs_colors = [UIColor.clear.cgColor, UIColor.clear.cgColor] + rankLabel.textColor = .FCD_68_D + } + case .purple: + if rank <= 3 { + rankBadgeView.xs_colors = [UIColor.E_7_CCFF.cgColor, UIColor.D_7_BCFF.cgColor, UIColor.A_191_FF.cgColor] + rankLabel.textColor = .black + } else { + rankBadgeView.xs_colors = [UIColor.clear.cgColor, UIColor.clear.cgColor] + rankLabel.textColor = UIColor.B_4_A_0_FF + } + } + } +} + +extension XSSearchHotListItemView { + + private func xs_setupUI() { + let tap = UITapGestureRecognizer(target: self, action: #selector(xs_handleTap)) + addGestureRecognizer(tap) + isUserInteractionEnabled = true + + addSubview(rankBadgeView) + rankBadgeView.addSubview(rankLabel) + addSubview(coverImageView) + addSubview(titleLabel) + addSubview(categoryLabel) + + rankBadgeView.snp.makeConstraints { make in + make.left.equalToSuperview() + make.centerY.equalToSuperview() + make.size.equalTo(CGSize(width: 14, height: 14)) + } + + rankLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + coverImageView.snp.makeConstraints { make in + make.left.equalTo(rankBadgeView.snp.right).offset(8) + make.centerY.equalToSuperview() + make.size.equalTo(CGSize(width: 48, height: 65)) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(10) + make.top.equalToSuperview() + make.right.lessThanOrEqualToSuperview() + } + + categoryLabel.snp.makeConstraints { make in + make.left.equalTo(titleLabel) + make.top.equalTo(titleLabel.snp.bottom).offset(6) + make.right.lessThanOrEqualToSuperview() + } + } +} + +extension XSSearchHotListItemView { + + @objc private func xs_handleTap() { + guard let model = model else { return } + didTap?(model) + } +} diff --git a/XSeri/Class/Home/View/XSSearchHotSectionView.swift b/XSeri/Class/Home/View/XSSearchHotSectionView.swift new file mode 100644 index 0000000..f0a1b68 --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchHotSectionView.swift @@ -0,0 +1,115 @@ +// +// XSSearchHotSectionView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +final class XSSearchHotSectionView: UIView { + + var title: String? { + didSet { + titleLabel.text = title + } + } + + /// 榜单板块数据 + var sections: [XSSearchHotSection] = [] { + didSet { + xs_reloadSections() + } + } + + /// 点击榜单条目回调 + var didSelectItem: ((_ model: XSShortModel) -> Void)? + + private var cardViews: [XSSearchHotListCardView] = [] + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .white + return label + }() + + private lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.showsHorizontalScrollIndicator = false + view.keyboardDismissMode = .onDrag + return view + }() + + private let contentView = UIView() + + private lazy var stackView: UIStackView = { + let view = UIStackView() + view.axis = .horizontal + view.spacing = 22 + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSSearchHotSectionView { + + private func xs_setupUI() { + addSubview(titleLabel) + addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(stackView) + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.top.equalToSuperview() + } + + scrollView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(12) + make.left.right.bottom.equalToSuperview() + make.height.equalTo(435) + } + + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + make.height.equalToSuperview() + } + + stackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.left.equalToSuperview().offset(16) + make.right.equalToSuperview().offset(-16) + make.height.equalTo(435) + } + } + + private func xs_reloadSections() { + cardViews.forEach { $0.removeFromSuperview() } + cardViews = sections.map { _ in XSSearchHotListCardView() } + stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + for (index, section) in sections.enumerated() { + let cardView = cardViews[index] + cardView.section = section + cardView.didSelectItem = { [weak self] model in + self?.didSelectItem?(model) + } + stackView.addArrangedSubview(cardView) + cardView.snp.makeConstraints { make in + make.width.equalTo(237) + make.height.equalTo(435) + } + } + } +} diff --git a/XSeri/Class/Home/View/XSSearchRecentView.swift b/XSeri/Class/Home/View/XSSearchRecentView.swift new file mode 100644 index 0000000..7ce52d5 --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchRecentView.swift @@ -0,0 +1,101 @@ +// +// XSSearchRecentView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +final class XSSearchRecentView: UIView { + + /// 清空历史回调 + var didTapClear: (() -> Void)? + /// 点击历史标签回调 + var didSelectTag: ((_ text: String) -> Void)? { + didSet { + tagsView.didSelectTag = didSelectTag + } + } + + var title: String? { + didSet { + titleLabel.text = title + } + } + + /// 最近搜索标签数据 + var tags: [String] = [] { + didSet { + tagsView.tags = tags + } + } + + private var tagsHeightConstraint: Constraint? + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .white + return label + }() + + private lazy var clearButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(named: "delete_icon_01"), for: .normal) + button.tintColor = .white.withAlphaComponent(0.6) + button.addTarget(self, action: #selector(xs_handleClear), for: .touchUpInside) + return button + }() + + private lazy var tagsView: XSSearchTagsView = { + let view = XSSearchTagsView() + view.didChangeHeight = { [weak self] height in + self?.tagsHeightConstraint?.update(offset: height) + } + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSSearchRecentView { + + private func xs_setupUI() { + addSubview(titleLabel) + addSubview(clearButton) + addSubview(tagsView) + + titleLabel.snp.makeConstraints { make in + make.left.top.equalToSuperview() + } + + clearButton.snp.makeConstraints { make in + make.centerY.equalTo(titleLabel) + make.right.equalToSuperview() + make.size.equalTo(CGSize(width: 20, height: 20)) + } + + tagsView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(titleLabel.snp.bottom).offset(12) + tagsHeightConstraint = make.height.equalTo(0).constraint + make.bottom.equalToSuperview() + } + } +} + +extension XSSearchRecentView { + + @objc private func xs_handleClear() { + didTapClear?() + } +} diff --git a/XSeri/Class/Home/View/XSSearchResultCell.swift b/XSeri/Class/Home/View/XSSearchResultCell.swift new file mode 100644 index 0000000..2531426 --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchResultCell.swift @@ -0,0 +1,63 @@ +// +// XSSearchResultCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +final class XSSearchResultCell: UICollectionViewCell { + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.placeholderColor = ._1_F_0_B_00 + imageView.layer.cornerRadius = 8 + imageView.layer.borderWidth = 1 + imageView.layer.borderColor = UIColor.white.withAlphaComponent(0.25).cgColor + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .bold) + label.textColor = .white + label.numberOfLines = 2 + label.lineBreakMode = .byTruncatingTail + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(model: XSShortModel) { + coverImageView.xs_setImage(model.image_url) + titleLabel.text = model.name ?? model.xs_description + } +} + +extension XSSearchResultCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + contentView.addSubview(titleLabel) + + coverImageView.snp.makeConstraints { make in + make.left.top.right.equalToSuperview() + make.height.equalTo(146) + } + + titleLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(coverImageView.snp.bottom).offset(8) + make.bottom.lessThanOrEqualToSuperview() + } + } +} diff --git a/XSeri/Class/Home/View/XSSearchSuggestionCell.swift b/XSeri/Class/Home/View/XSSearchSuggestionCell.swift new file mode 100644 index 0000000..db8c377 --- /dev/null +++ b/XSeri/Class/Home/View/XSSearchSuggestionCell.swift @@ -0,0 +1,146 @@ +// +// XSSearchSuggestionCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/2. +// + +import UIKit +import SnapKit + +final class XSSearchSuggestionCell: UICollectionViewCell { + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.placeholderColor = ._1_F_0_B_00 + imageView.layer.cornerRadius = 7 + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .semibold) + label.textColor = UIColor.white.withAlphaComponent(0.4) + label.numberOfLines = 1 + label.lineBreakMode = .byTruncatingTail + return label + }() + + private lazy var tagContainer: UIView = { + let view = UIView() + view.backgroundColor = UIColor.white.withAlphaComponent(0.1) + view.layer.cornerRadius = 6 + view.layer.masksToBounds = true + return view + }() + + private lazy var tagLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .regular) + label.textColor = .white + return label + }() + + private lazy var epLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .regular) + label.textColor = .white + return label + }() + + private lazy var infoStack: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 23 + stackView.alignment = .leading + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(model: XSShortModel, keyword: String) { + coverImageView.xs_setImage(model.image_url) + let title = model.name ?? model.xs_description ?? "" + titleLabel.attributedText = xs_highlight(text: title, keyword: keyword) + let category = model.category?.first ?? model.categoryList?.first?.name ?? "" + tagLabel.text = category + tagContainer.isHidden = category.isEmpty + if let current = model.current_episode, !current.isEmpty { + epLabel.text = "EP.##".localizedReplace(text: current) + } else if let total = model.episode_total { + epLabel.text = "EP.##".localizedReplace(text: "\(total)") + } else { + epLabel.text = "" + } + epLabel.isHidden = (epLabel.text ?? "").isEmpty + } +} + +extension XSSearchSuggestionCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + contentView.addSubview(infoStack) + tagContainer.addSubview(tagLabel) + + infoStack.addArrangedSubview(titleLabel) + infoStack.addArrangedSubview(tagContainer) + infoStack.addArrangedSubview(epLabel) + + coverImageView.snp.makeConstraints { make in + make.left.top.bottom.equalToSuperview() + make.size.equalTo(CGSize(width: 79, height: 104)) + } + + infoStack.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(12) + make.centerY.equalToSuperview() + make.right.lessThanOrEqualToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.width.equalToSuperview() + } + + tagContainer.snp.makeConstraints { make in + make.height.equalTo(20) + } + + tagLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(12) + make.right.equalToSuperview().offset(-12) + } + } + + private func xs_highlight(text: String, keyword: String) -> NSAttributedString { + let baseColor = UIColor.white.withAlphaComponent(0.4) + let highlightColor = UIColor.white + let attributed = NSMutableAttributedString( + string: text, + attributes: [ + .foregroundColor: baseColor + ] + ) + + guard !keyword.isEmpty else { return attributed } + let lowerText = text.lowercased() + let lowerKeyword = keyword.lowercased() + var searchRange = lowerText.startIndex.. Void)? + /// 标签点击回调 + var didSelectTag: ((_ text: String) -> Void)? + + private var lastLayoutWidth: CGFloat = 0 + + /// 标签数据列表 + var tags: [String] = [] { + didSet { + collectionView.reloadData() + } + } + + private lazy var layout: TagsLayout = { + let layout = TagsLayout() + layout.delegate = self + layout.cellsPadding = .init(horizontal: 12, vertical: 12) + layout.contentPadding = .init(horizontal: 0, vertical: 0) + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.isScrollEnabled = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.addObserver(self, forKeyPath: "contentSize", context: nil) + collectionView.register(XSSearchTagCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + deinit { + collectionView.removeObserver(self, forKeyPath: "contentSize") + } + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard keyPath == "contentSize" else { return } + didChangeHeight?(collectionView.contentSize.height) + } + + override func layoutSubviews() { + super.layoutSubviews() + // 宽度确定后刷新布局,避免标签被纵向堆叠 + if bounds.width != lastLayoutWidth { + lastLayoutWidth = bounds.width + collectionView.collectionViewLayout.invalidateLayout() + } + } +} + +extension XSSearchTagsView { + + private func xs_setupUI() { + addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} + +// MARK: - UICollectionViewDelegate UICollectionViewDataSource +extension XSSearchTagsView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return tags.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSSearchTagCell + cell.title = tags[indexPath.row] + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + didSelectTag?(tags[indexPath.row]) + } +} + +// MARK: - LayoutDelegate +extension XSSearchTagsView: LayoutDelegate { + + func cellSize(indexPath: IndexPath) -> CGSize { + let text = tags[indexPath.row] + let width = text.size(XSSearchTagCell.textFont, .init(width: XSScreen.width, height: 24)).width + return .init(width: width + 20, height: 24) + } +} diff --git a/XSeri/Class/Home/ViewModel/XSHomeViewModel.swift b/XSeri/Class/Home/ViewModel/XSHomeViewModel.swift new file mode 100644 index 0000000..0b29791 --- /dev/null +++ b/XSeri/Class/Home/ViewModel/XSHomeViewModel.swift @@ -0,0 +1,41 @@ +// +// XSHomeViewModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit + +@MainActor +class XSHomeViewModel: NSObject { + + var dataArr: [XSHomeModuleItem] = [] + + + func requestHomeData() async { + guard let list = await XSHomeAPI.requestHomeData() else { return } + self.dataArr.removeAll() + + var popularItem: XSHomeModuleItem? + var rankingsItem: XSHomeModuleItem? + + list.forEach { + if $0.module_key == .week_highest_recommend { + popularItem = $0 + } else if $0.module_key == .week_ranking { + rankingsItem = $0 + } + } + + if let item = popularItem { + self.dataArr.append(item) + } + + if let item = rankingsItem { + self.dataArr.append(item) + } + + } + +} diff --git a/XSeri/Class/Mine/Controller/XSAboutViewController.swift b/XSeri/Class/Mine/Controller/XSAboutViewController.swift new file mode 100644 index 0000000..ca68a3c --- /dev/null +++ b/XSeri/Class/Mine/Controller/XSAboutViewController.swift @@ -0,0 +1,94 @@ +// +// XSAboutViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import SnapKit + +class XSAboutViewController: XSCommonViewController { + + private lazy var dataArr: [XSMineItem] = { + let arr = [ + XSMineItem(type: .web, title: "Privacy Policy".localized, url: kXSPrivacyPolicyWebUrl), + XSMineItem(type: .web, title: "User Agreement".localized, url: kXSUserAgreementWebUrl), + XSMineItem(type: .safari, title: "Visit Website".localized, url: XSWebBaseURL), + ] + return arr + }() + + private lazy var tableView: XSTableView = { + let tableView = XSTableView(frame: .zero, style: .grouped) + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.register(XSAboutCell.self, forCellReuseIdentifier: "cell") + tableView.register(XSAboutHeaderView.self, forHeaderFooterViewReuseIdentifier: "header") + return tableView + }() + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "About".localized + + xs_setupUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(false, animated: true) + self.xs_setNavigationStyle() + } + +} + +extension XSAboutViewController { + + private func xs_setupUI() { + view.addSubview(tableView) + + tableView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalTo(self.view.safeAreaLayoutGuide) + } + } + +} + + +//MARK: UITableViewDelegate UITableViewDataSource +extension XSAboutViewController: UITableViewDelegate, UITableViewDataSource { + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! XSAboutCell + cell.item = self.dataArr[indexPath.row] + return cell + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.dataArr.count + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as? XSAboutHeaderView + return view + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let item = self.dataArr[indexPath.row] + guard let urlStr = item.url else { return } + + if item.type == .web { + let vc = XSBaseWebViewController() + vc.webUrl = urlStr + self.navigationController?.pushViewController(vc, animated: true) + } else { + if let url = URL(string: urlStr) { + UIApplication.shared.open(url) + } + } + } + +} diff --git a/XSeri/Class/Mine/Controller/XSFeedbackViewController.swift b/XSeri/Class/Mine/Controller/XSFeedbackViewController.swift new file mode 100644 index 0000000..67ac9b9 --- /dev/null +++ b/XSeri/Class/Mine/Controller/XSFeedbackViewController.swift @@ -0,0 +1,97 @@ +// +// XSFeedbackViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/28. +// + +import UIKit +import SnapKit + +class XSFeedbackViewController: XSAppWebViewController { + + + private lazy var rightButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "feedback_icon_02"), for: .normal) + button.addTarget(self, action: #selector(handleRightBarButton), for: .touchUpInside) + return button + }() + + private lazy var redView: UIView = { + let view = UIView() + view.backgroundColor = .FF_313_B + view.layer.cornerRadius = 8 + view.isHidden = true + return view + }() + + private lazy var redLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .bold) + label.textColor = .white + return label + }() + + + override func viewDidLoad() { + self.webUrl = kXSFeedBackHomeWebUrl + super.viewDidLoad() + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightButton) + + xs_setupUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + Task { + await self.requestRedCount() + } + } + + @objc private func handleRightBarButton() { + let vc = XSAppWebViewController() + vc.webUrl = kXSFeedBackListWebUrl + self.navigationController?.pushViewController(vc, animated: true) + + } + +} + +extension XSFeedbackViewController { + + private func xs_setupUI() { + + rightButton.addSubview(redView) + redView.addSubview(redLabel) + + redView.snp.makeConstraints { make in + make.height.equalTo(16) + make.width.greaterThanOrEqualTo(16) + make.top.equalToSuperview().offset(-8) + make.right.equalToSuperview().offset(8) + } + + redLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + make.left.greaterThanOrEqualToSuperview().offset(3) + } + } + +} + +extension XSFeedbackViewController { + + private func requestRedCount() async { + guard let model = await XSSettingAPI.requestFeedbackRedCount() else { return } + + if let count = model.feedback_notice_num, count > 0 { + self.redView.isHidden = false + self.redLabel.text = "\(count)" + } else { + self.redView.isHidden = true + } + + } +} diff --git a/XSeri/Class/Mine/Controller/XSMineViewController.swift b/XSeri/Class/Mine/Controller/XSMineViewController.swift new file mode 100644 index 0000000..622202b --- /dev/null +++ b/XSeri/Class/Mine/Controller/XSMineViewController.swift @@ -0,0 +1,161 @@ +// +// XSMineViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import SnapKit + +class XSMineViewController: XSCommonViewController { + + private lazy var dataArr: [XSMineItem] = [] + + private lazy var historyDataArr: [XSShortModel] = [] + + private lazy var logoImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "logo_icon_01")) + return imageView + }() + + private lazy var tableView: XSTableView = { + let tableView = XSTableView(frame: .zero, style: .grouped) + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.rowHeight = 44 + tableView.xs_addRefreshHeader { [weak self] in + guard let self = self else { return } + self.handleHeaderRefresh(nil) + } + tableView.register(XSMineCell.self, forCellReuseIdentifier: "cell") + tableView.register(XSMineHeaderView.self, forHeaderFooterViewReuseIdentifier: "header") + return tableView + }() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(networkStatusDidChangeNotification), name: XSNetworkMonitorManager.networkStatusDidChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: XSLoginManager.userInfoUpdateNotification, object: nil) + + self.createDataArr() + xs_setupUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.createDataArr() + Task { + await XSLoginManager.manager.updateUserInfo() + + await self.requestPlayHistory() + } + } + + override func handleHeaderRefresh(_ completer: (() -> Void)?) { + Task { + await XSLoginManager.manager.updateUserInfo() + await self.requestPlayHistory() + self.tableView.xs_endHeaderRefreshing() + } + } + + @objc private func userInfoUpdateNotification() { + self.tableView.reloadData() + } + + @objc private func networkStatusDidChangeNotification() { + Task { + await XSLoginManager.manager.updateUserInfo() + await self.requestPlayHistory() + } + } + +} + +extension XSMineViewController { + + private func xs_setupUI() { + view.addSubview(logoImageView) + view.addSubview(self.tableView) + + logoImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.top.equalTo(self.view.safeAreaLayoutGuide).offset(9) + } + + self.tableView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalTo(logoImageView.snp.bottom).offset(16) + } + } + +} + +//MARK: UITableViewDelegate UITableViewDataSource +extension XSMineViewController: UITableViewDelegate, UITableViewDataSource { + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! XSMineCell + cell.item = self.dataArr[indexPath.row] + return cell + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.dataArr.count + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as? XSMineHeaderView + view?.userInfo = XSLoginManager.manager.userInfo + view?.historyDataArr = self.historyDataArr + view?.updateLayout() + return view + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let item = self.dataArr[indexPath.row] + switch item.type { + case .about: + let vc = XSAboutViewController() + self.navigationController?.pushViewController(vc, animated: true) + + case .feedback: + let vc = XSFeedbackViewController() + self.navigationController?.pushViewController(vc, animated: true) + + default: + break + } + } +} + +extension XSMineViewController { + + private func createDataArr() { + let arr = [ +// XSMineItem(type: .setting, iconImage: UIImage(named: "setting_icon_01"), title: "Setting".localized), + XSMineItem(type: .about, iconImage: UIImage(named: "about_icon_01"), title: "About".localized), + XSMineItem(type: .feedback, iconImage: UIImage(named: "feedback_icon_01"), title: "Feedback".localized) + ] + self.dataArr = arr + self.tableView.reloadData() + } + + private func requestPlayHistory() async { + guard let list = await XSVideoAPI.requestPlayHistorys(page: 1) else { return } + self.historyDataArr = list + self.tableView.reloadData() + } + +} diff --git a/XSeri/Class/Mine/Model/XSFeedbackCountModel.swift b/XSeri/Class/Mine/Model/XSFeedbackCountModel.swift new file mode 100644 index 0000000..928a222 --- /dev/null +++ b/XSeri/Class/Mine/Model/XSFeedbackCountModel.swift @@ -0,0 +1,13 @@ +// +// XSFeedbackCountModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/28. +// + +import UIKit +import SmartCodable + +struct XSFeedbackCountModel: SmartCodable { + var feedback_notice_num: Int? +} diff --git a/XSeri/Class/Mine/Model/XSMineItem.swift b/XSeri/Class/Mine/Model/XSMineItem.swift new file mode 100644 index 0000000..299898b --- /dev/null +++ b/XSeri/Class/Mine/Model/XSMineItem.swift @@ -0,0 +1,27 @@ +// +// XSMineItem.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit + +struct XSMineItem { + + enum ItemType { + case setting + case about + case feedback + case web + case safari + } + + + var type: ItemType? + var iconImage: UIImage? + var title: String? + var url: String? + + +} diff --git a/XSeri/Class/Mine/View/XSAboutCell.swift b/XSeri/Class/Mine/View/XSAboutCell.swift new file mode 100644 index 0000000..cff6555 --- /dev/null +++ b/XSeri/Class/Mine/View/XSAboutCell.swift @@ -0,0 +1,62 @@ +// +// XSAboutCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import SnapKit + +class XSAboutCell: XSTableViewCell { + + var item: XSMineItem? { + didSet { + titleLabel.text = item?.title + } + } + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .semibold) + label.textColor = .white + return label + }() + + private lazy var lineView: UIView = { + let view = UIView() + view.backgroundColor = .white.withAlphaComponent(0.1) + return view + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + xs_indicatorImageView.image = UIImage(named: "arrow_right_icon_02") + + contentView.addSubview(titleLabel) + contentView.addSubview(xs_indicatorImageView) + contentView.addSubview(lineView) + + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(30) + } + + xs_indicatorImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-30) + } + + lineView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(30) + make.centerX.equalToSuperview() + make.bottom.equalToSuperview() + make.height.equalTo(0.6) + } + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/XSeri/Class/Mine/View/XSAboutHeaderView.swift b/XSeri/Class/Mine/View/XSAboutHeaderView.swift new file mode 100644 index 0000000..1318305 --- /dev/null +++ b/XSeri/Class/Mine/View/XSAboutHeaderView.swift @@ -0,0 +1,64 @@ +// +// XSAboutHeaderView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import SnapKit + +class XSAboutHeaderView: UITableViewHeaderFooterView { + + private lazy var logoImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "logo_icon_02")) + return imageView + }() + + private lazy var appNameLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 18, weight: .semibold) + label.textColor = .white + label.text = kXSName + return label + }() + + private lazy var versionLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .FDE_7_B_8 + label.text = "Version " + kXSVersion + return label + }() + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + automaticallyUpdatesBackgroundConfiguration = false + + contentView.addSubview(logoImageView) + contentView.addSubview(appNameLabel) + contentView.addSubview(versionLabel) + + logoImageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalToSuperview().offset(30) + } + + appNameLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalTo(logoImageView.snp.bottom).offset(7) + } + + versionLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalTo(appNameLabel.snp.bottom).offset(7) + make.bottom.equalToSuperview().offset(-30) + } + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/XSeri/Class/Mine/View/XSMineCell.swift b/XSeri/Class/Mine/View/XSMineCell.swift new file mode 100644 index 0000000..0a261e2 --- /dev/null +++ b/XSeri/Class/Mine/View/XSMineCell.swift @@ -0,0 +1,66 @@ +// +// XSMineCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import SnapKit + +class XSMineCell: XSTableViewCell { + + var item: XSMineItem? { + didSet { + iconImageView.image = item?.iconImage + titleLabel.text = item?.title + } + } + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .FFDAA_4 + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + xs_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSMineCell { + + private func xs_setupUI() { + contentView.addSubview(iconImageView) + contentView.addSubview(titleLabel) + contentView.addSubview(xs_indicatorImageView) + + iconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(16) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(44) + } + + xs_indicatorImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-16) + } + } + +} diff --git a/XSeri/Class/Mine/View/XSMineHeaderView.swift b/XSeri/Class/Mine/View/XSMineHeaderView.swift new file mode 100644 index 0000000..35978a7 --- /dev/null +++ b/XSeri/Class/Mine/View/XSMineHeaderView.swift @@ -0,0 +1,78 @@ +// +// XSMineHeaderView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/12. +// + +import UIKit +import SnapKit + +class XSMineHeaderView: UITableViewHeaderFooterView { + + var userInfo: XSUserInfo? { + didSet { + userInfoView.userInfo = userInfo + } + } + + var historyDataArr: [XSShortModel] = [] { + didSet { + historyView.dataArr = historyDataArr + } + } + + + + private lazy var stackView: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.spacing = 16 + return view + }() + + private lazy var userInfoView: XSMineUserInfoView = { + let view = XSMineUserInfoView() + return view + }() + + private lazy var historyView: XSMinePlayHistoryView = { + let view = XSMinePlayHistoryView() + return view + }() + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateLayout() { + stackView.xs_removeAllArrangedSubview() + + stackView.addArrangedSubview(userInfoView) + + if !historyDataArr.isEmpty { + stackView.addArrangedSubview(historyView) + } + } + +} + +extension XSMineHeaderView { + + private func xs_setupUI() { + contentView.addSubview(stackView) + + stackView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(6) + make.bottom.equalToSuperview().offset(-20) + make.left.right.equalToSuperview() + } + } + +} diff --git a/XSeri/Class/Mine/View/XSMinePlayHistoryCell.swift b/XSeri/Class/Mine/View/XSMinePlayHistoryCell.swift new file mode 100644 index 0000000..a0615a9 --- /dev/null +++ b/XSeri/Class/Mine/View/XSMinePlayHistoryCell.swift @@ -0,0 +1,86 @@ +// +// XSMinePlayHistoryCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/26. +// + +import UIKit +import SnapKit + +class XSMinePlayHistoryCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + + let epString = NSMutableAttributedString(string: "EP.\(model?.current_episode ?? "0")") + epString.yy_color = .FFDAA_4 + + let totalEpString = NSMutableAttributedString(string: "/\(model?.episode_total ?? 0)") + totalEpString.yy_color = .white.withAlphaComponent(0.4) + + epString.append(totalEpString) + + epLabel.attributedText = epString + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 4 + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .medium) + label.textColor = .white + label.numberOfLines = 2 + return label + }() + + private lazy var epLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .medium) + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSMinePlayHistoryCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + contentView.addSubview(titleLabel) + contentView.addSubview(epLabel) + + coverImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview().offset(-58) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview() + make.top.equalTo(coverImageView.snp.bottom).offset(12) + make.right.lessThanOrEqualToSuperview() + } + + epLabel.snp.makeConstraints { make in + make.left.equalToSuperview() + make.bottom.equalToSuperview() + } + } + +} diff --git a/XSeri/Class/Mine/View/XSMinePlayHistoryView.swift b/XSeri/Class/Mine/View/XSMinePlayHistoryView.swift new file mode 100644 index 0000000..c10ea3c --- /dev/null +++ b/XSeri/Class/Mine/View/XSMinePlayHistoryView.swift @@ -0,0 +1,129 @@ +// +// XSMinePlayHistoryView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/26. +// + +import UIKit +import SnapKit + +class XSMinePlayHistoryView: UIView { + + + var dataArr: [XSShortModel] = [] { + didSet { + self.collectionView.reloadData() + } + } + + private lazy var moreButton: UIControl = { + let button = UIButton(primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + self.viewController?.tabBarController?.selectedIndex = 2 + })) + return button + }() + + private lazy var iconImageView = UIImageView(image: UIImage(named: "history_icon_01")) + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .FFDAA_4 + label.text = "Browsing History".localized + return label + }() + + private lazy var indicatorImageView = UIImageView(image: UIImage(named: "arrow_right_icon_01")) + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = .init(width: 109, height: 203) + layout.minimumLineSpacing = 8 + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.contentInset = .init(top: 0, left: 16, bottom: 0, right: 16) + collectionView.register(XSMinePlayHistoryCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSMinePlayHistoryView { + + private func xs_setupUI() { + addSubview(moreButton) + moreButton.addSubview(iconImageView) + moreButton.addSubview(titleLabel) + moreButton.addSubview(indicatorImageView) + addSubview(collectionView) + + moreButton.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalToSuperview() + make.height.equalTo(20) + } + + iconImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.centerY.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(iconImageView.snp.right).offset(8) + } + + indicatorImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-16) + } + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(moreButton.snp.bottom).offset(23) + make.height.equalTo(self.collectionViewLayout.itemSize.height) + make.bottom.equalToSuperview() + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSMinePlayHistoryView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSMinePlayHistoryCell + cell.model = self.dataArr[indexPath.row] + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = self.dataArr[indexPath.row] + let vc = XSShortDetailViewController() + vc.shortId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } +} diff --git a/XSeri/Class/Mine/View/XSMineUserInfoView.swift b/XSeri/Class/Mine/View/XSMineUserInfoView.swift new file mode 100644 index 0000000..f37e714 --- /dev/null +++ b/XSeri/Class/Mine/View/XSMineUserInfoView.swift @@ -0,0 +1,79 @@ +// +// XSMineUserInfoView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/26. +// + +import UIKit +import SnapKit + +class XSMineUserInfoView: UIView { + + var userInfo: XSUserInfo? { + didSet { + avatarImageView.xs_setImage(userInfo?.avator, placeholder: UIImage(named: "avatar_placeholder_icon_01")) + nickNameLabel.text = userInfo?.getNickName() + idLabel.text = "ID: \(userInfo?.customer_id ?? "")" + } + } + + private lazy var avatarImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 28 + return imageView + }() + + private lazy var nickNameLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 16, weight: .bold) + label.textColor = .white + return label + }() + + private lazy var idLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .medium) + label.textColor = .white.withAlphaComponent(0.6) + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSMineUserInfoView { + + private func xs_setupUI() { + addSubview(avatarImageView) + addSubview(nickNameLabel) + addSubview(idLabel) + + avatarImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.top.equalToSuperview().offset(4) + make.height.width.equalTo(56) + make.bottom.equalToSuperview().offset(-4) + } + + nickNameLabel.snp.makeConstraints { make in + make.left.equalTo(avatarImageView.snp.right).offset(10) + make.top.equalTo(avatarImageView).offset(8) + } + + idLabel.snp.makeConstraints { make in + make.left.equalTo(nickNameLabel) + make.bottom.equalTo(avatarImageView).offset(-6) + } + + } + +} diff --git a/XSeri/Class/MyList/Controller/XSMyCollectViewController.swift b/XSeri/Class/MyList/Controller/XSMyCollectViewController.swift new file mode 100644 index 0000000..9cee341 --- /dev/null +++ b/XSeri/Class/MyList/Controller/XSMyCollectViewController.swift @@ -0,0 +1,167 @@ +// +// XSMyCollectViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/25. +// + +import UIKit +import SnapKit +import LYEmptyView + +class XSMyCollectViewController: XSViewController { + + private lazy var dataArr: [XSShortModel] = [] + private lazy var page: Int = 1 + + private lazy var xs_isEditing: Bool = false { + didSet { + self.collectionView.reloadData() + } + } + + private lazy var bgImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "bg_image_02")) + return imageView + }() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let width = (XSScreen.width - 30 - 12) / 3 + let height = 148 / 111 * width + 22 + + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: floor(width), height: height) + layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15) + layout.minimumInteritemSpacing = 6 + layout.minimumLineSpacing = 18 + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 10, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0) + collectionView.ly_emptyView = XSEmpty.xs_emptyView() + collectionView.xs_addRefreshHeader(insetTop: collectionView.contentInset.top) { [weak self] in + self?.handleHeaderRefresh(nil) + } + collectionView.xs_addRefreshFooter(insetBottom: collectionView.contentInset.bottom) { [weak self] in + self?.handleFooterRefresh(nil) + } + collectionView.register(XSMyCollectCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "My Favorites".localized + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "edit_icon_01"), style: .plain, target: self, action: #selector(handleRightBarButton)) + + xs_setupUI() + + Task { + await self.requestDataArr(page: 1) + } + } + + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(false, animated: true) + xs_setNavigationStyle() + } + + override func handleHeaderRefresh(_ completer: (() -> Void)?) { + Task { + await self.requestDataArr(page: 1) + self.collectionView.xs_endHeaderRefreshing() + } + } + + override func handleFooterRefresh(_ completer: (() -> Void)?) { + Task { + await self.requestDataArr(page: self.page + 1) + self.collectionView.xs_endFooterRefreshing() + } + } + + @objc private func handleRightBarButton() { + self.xs_isEditing = !self.xs_isEditing + } + +} + +extension XSMyCollectViewController { + + private func xs_setupUI() { + view.addSubview(bgImageView) + view.addSubview(collectionView) + + bgImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + collectionView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalTo(self.view.safeAreaLayoutGuide) + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSMyCollectViewController: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSMyCollectCell + cell.model = self.dataArr[indexPath.row] + cell.isEditing = self.xs_isEditing + cell.clickDeleteButton = { [weak self] cell in + guard let self = self else { return } + guard let indexPath = self.collectionView.indexPath(for: cell) else { return } + let model = self.dataArr[indexPath.row] + guard let shortId = model.short_play_id else { return } + + Task { + if await XSVideoAPI.requestCollectShort(isCollect: false, shortId: shortId, videoId: model.short_play_video_id) { + self.dataArr.remove(at: indexPath.row) + self.collectionView.deleteItems(at: [indexPath]) + } + } + + } + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if self.xs_isEditing { return } + let model = self.dataArr[indexPath.row] + + let vc = XSShortDetailViewController() + vc.shortId = model.short_play_id + self.navigationController?.pushViewController(vc, animated: true) + } +} + +extension XSMyCollectViewController { + + private func requestDataArr(page: Int) async { + guard let list = await XSVideoAPI.requestCollectList(page: page) else { return } + + if page == 1 { + self.dataArr.removeAll() + } + self.dataArr += list + self.page = page + + self.collectionView.reloadData() + + } + +} diff --git a/XSeri/Class/MyList/Controller/XSMyListViewController.swift b/XSeri/Class/MyList/Controller/XSMyListViewController.swift new file mode 100644 index 0000000..40a1392 --- /dev/null +++ b/XSeri/Class/MyList/Controller/XSMyListViewController.swift @@ -0,0 +1,169 @@ +// +// XSMyListViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/13. +// + +import UIKit +import SnapKit +import LYEmptyView + +class XSMyListViewController: XSViewController { + + + private lazy var collectsDataArr: [XSShortModel] = [] + + private lazy var historyDataArr: [XSShortModel] = [] + private lazy var historyPage = 1 + + private lazy var isNeedUpdateCollect = false + + private lazy var bgImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "bg_image_02")) + return imageView + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 14 + return stackView + }() + + private lazy var collectsView: XSMyListCollectsView = { + let view = XSMyListCollectsView() + return view + }() + + private lazy var historyView: XSMyListHistoryView = { + let view = XSMyListHistoryView() + view.refreshHeaderBlock = { [weak self] in + guard let self = self else { return } + Task { + await self.requestHistory(page: 1) + self.historyView.endHeaderRefreshing() + } + } + view.refreshFooterBlock = { [weak self] in + guard let self = self else { return } + Task { + await self.requestHistory(page: self.historyPage + 1) + self.historyView.endFooterRefreshing() + } + } + + return view + }() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "My List".localized + + NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: XSVideoAPI.updateShortCollectStateNotification, object: nil) + + + + xs_setupUI() + + Task { + await requestCollects() + await requestHistory(page: 1) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(false, animated: true) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if isNeedUpdateCollect { + Task { + await requestCollects() + } + } + } + + + @objc private func updateShortCollectStateNotification() { + isNeedUpdateCollect = true + } + + + private func updateLayout() { + self.stackView.xs_removeAllArrangedSubview() + + if !self.collectsDataArr.isEmpty { + stackView.addArrangedSubview(collectsView) + } + + if !self.historyDataArr.isEmpty { + stackView.addArrangedSubview(historyView) + } + + if self.stackView.arrangedSubviews.count > 0 { + self.view.ly_hideEmpty() + } else { + self.view.ly_showEmpty() + } + + } + +} + +extension XSMyListViewController { + + private func xs_setupUI() { + view.ly_emptyView = XSEmpty.xs_emptyView() + view.ly_emptyView.autoShowEmptyView = false + view.ly_hideEmpty() + + view.addSubview(bgImageView) + view.addSubview(stackView) + + bgImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + stackView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.lessThanOrEqualToSuperview() + make.top.equalTo(self.view.safeAreaLayoutGuide).offset(10) + } + } + +} + +extension XSMyListViewController { + + private func requestHistory(page: Int) async { + guard let list = await XSVideoAPI.requestPlayHistorys(page: page) else { return } + + if page == 1 { + self.historyDataArr.removeAll() + } + self.historyPage = page + self.historyDataArr += list + + self.historyView.dataArr = self.historyDataArr + + self.updateLayout() + } + + private func requestCollects() async { + guard let list = await XSVideoAPI.requestCollectList(page: 1, pageSize: 3) else { return } + + self.collectsDataArr = list + + self.collectsView.dataArr = list + + self.updateLayout() + } + +} diff --git a/XSeri/Class/MyList/View/XSMyCollectCell.swift b/XSeri/Class/MyList/View/XSMyCollectCell.swift new file mode 100644 index 0000000..6dfb366 --- /dev/null +++ b/XSeri/Class/MyList/View/XSMyCollectCell.swift @@ -0,0 +1,95 @@ +// +// XSMyCollectCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/25. +// + +import UIKit +import SnapKit + +class XSMyCollectCell: UICollectionViewCell { + + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + titleLabel.text = model?.name + } + } + + var isEditing = false { + didSet { + deleteButton.isHidden = !isEditing + } + } + + var clickDeleteButton: ((_ cell: XSMyCollectCell) -> Void)? + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 12 + imageView.isUserInteractionEnabled = true + return imageView + }() + + private lazy var deleteButton: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(named: "delete_icon_02") + + + let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + self.clickDeleteButton?(self) + })) + button.isHidden = true + button.layer.borderColor = UIColor.white.withAlphaComponent(0.15).cgColor + button.layer.borderWidth = 1 + button.layer.cornerRadius = 12 + button.layer.masksToBounds = true + button.backgroundColor = .black.withAlphaComponent(0.65) + return button + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .medium) + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSMyCollectCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + coverImageView.addSubview(deleteButton) + contentView.addSubview(titleLabel) + + coverImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview().offset(-22) + } + + deleteButton.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.right.lessThanOrEqualToSuperview().offset(-2) + make.bottom.equalToSuperview() + } + } + +} diff --git a/XSeri/Class/MyList/View/XSMyListCollectsCell.swift b/XSeri/Class/MyList/View/XSMyListCollectsCell.swift new file mode 100644 index 0000000..546dfce --- /dev/null +++ b/XSeri/Class/MyList/View/XSMyListCollectsCell.swift @@ -0,0 +1,37 @@ +// +// XSMyListCollectsCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/13. +// + +import UIKit +import SnapKit + +class XSMyListCollectsCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 8 + 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") + } +} diff --git a/XSeri/Class/MyList/View/XSMyListCollectsView.swift b/XSeri/Class/MyList/View/XSMyListCollectsView.swift new file mode 100644 index 0000000..02c8c17 --- /dev/null +++ b/XSeri/Class/MyList/View/XSMyListCollectsView.swift @@ -0,0 +1,156 @@ +// +// XSMyListCollectsView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/13. +// + +import UIKit +import SnapKit + +class XSMyListCollectsView: UIView { + + var dataArr: [XSShortModel] = [] { + didSet { + self.collectionView.reloadData() + } + } + + private lazy var bgView: UIImageView = { + let view = UIImageView(image: UIImage(named: "collects_bg_image_01")) + view.isUserInteractionEnabled = true + return view + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 18, weight: .semibold) + label.textColor = .white + label.text = "My Favorites".localized + return label + }() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let itemWidth = floor((XSScreen.width - 30 - 20 - 20) / 3) + let itemHeight = 133 / 100 * itemWidth + + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: itemWidth, height: itemHeight) + layout.minimumLineSpacing = 10 + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 0, left: 10, bottom: 0, right: 10) + collectionView.isScrollEnabled = false + collectionView.register(XSMyListCollectsCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + private lazy var moreBgView: UIView = { + let view = XSView() + view.xs_colors = [UIColor.F_5_BD_7_E.cgColor, UIColor.FFEABC.cgColor, UIColor.FFCF_99.cgColor] + view.xs_startPoint = .init(x: 0, y: 0.5) + view.xs_endPoint = .init(x: 1, y: 0.5) + view.xs_setCornerRadius(topLeft: 8, topRight: 0, bottomLeft: 0, bottomRight: 15) + return view + }() + + private lazy var moreButton: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.contentInsets = .init(top: 0, leading: 12, bottom: 0, trailing: 12) + configuration.image = UIImage(named: "arrow_right_icon_03") + configuration.imagePadding = 10 + configuration.imagePlacement = .trailing + configuration.attributedTitle = AttributedString("See All".localized, attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 12, weight: .semibold), + .foregroundColor : UIColor._282828 + ])) + + let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + let vc = XSMyCollectViewController() + self.viewController?.navigationController?.pushViewController(vc, animated: true) + })) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension XSMyListCollectsView { + + private func xs_setupUI() { + addSubview(bgView) + bgView.addSubview(titleLabel) + bgView.addSubview(collectionView) + bgView.addSubview(moreBgView) + moreBgView.addSubview(moreButton) + + bgView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.centerX.equalToSuperview() + make.top.equalToSuperview() + make.bottom.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.top.equalToSuperview().offset(14) + } + + collectionView.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.equalToSuperview() + make.top.equalToSuperview().offset(51) + make.bottom.equalToSuperview().offset(-18) + make.height.equalTo(self.collectionViewLayout.itemSize.height) + } + + moreBgView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-1) + make.bottom.equalToSuperview().offset(-1) + make.height.equalTo(27) + make.width.greaterThanOrEqualTo(90) + } + + moreButton.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSMyListCollectsView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSMyListCollectsCell + cell.model = self.dataArr[indexPath.row] + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = self.dataArr[indexPath.row] + + let vc = XSShortDetailViewController() + vc.shortId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } + +} diff --git a/XSeri/Class/MyList/View/XSMyListHistoryCell.swift b/XSeri/Class/MyList/View/XSMyListHistoryCell.swift new file mode 100644 index 0000000..9921fe4 --- /dev/null +++ b/XSeri/Class/MyList/View/XSMyListHistoryCell.swift @@ -0,0 +1,139 @@ +// +// XSMyListHistoryCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/13. +// + +import UIKit +import SnapKit + +class XSMyListHistoryCell: UICollectionViewCell { + + var model: XSShortModel? { + didSet { + coverImageView.xs_setImage(model?.image_url) + nameLabel.text = model?.name + categoryLabel.text = model?.category?.first + + epLabel.text = "EP.\(model?.current_episode ?? "0")" + + collectButton.setNeedsUpdateConfiguration() + } + } + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 8 + return imageView + }() + + private lazy var nameLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 13, weight: .medium) + label.textColor = .white + return label + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .FFEC_80 + return label + }() + + private lazy var epLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .semibold) + label.textColor = .white + return label + }() + + private lazy var collectButton: UIButton = { + var configuration = UIButton.Configuration.plain() + let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + guard let shortId = self.model?.short_play_id else { return } + let isCollect = !(self.model?.is_collect ?? false) + Task { + await XSVideoAPI.requestCollectShort(isCollect: isCollect, shortId: shortId, videoId: self.model?.short_play_video_id) + } + })) + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + var newConfig = button.configuration + if self.model?.is_collect == true { + newConfig?.image = UIImage(named: "collect_icon_02_selected") + } else { + newConfig?.image = UIImage(named: "collect_icon_02") + } + button.configuration = newConfig + } + return button + }() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: XSVideoAPI.updateShortCollectStateNotification, object: nil) + + xs_setupUI() + } + + 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 shortPlayId = userInfo["id"] as? String else { return } + guard let state = userInfo["state"] as? Bool else { return } + guard shortPlayId == self.model?.short_play_id else { return } + self.model?.is_collect = state + self.collectButton.setNeedsUpdateConfiguration() + } + +} + +extension XSMyListHistoryCell { + + private func xs_setupUI() { + contentView.addSubview(coverImageView) + contentView.addSubview(nameLabel) + contentView.addSubview(categoryLabel) + contentView.addSubview(epLabel) + contentView.addSubview(collectButton) + + coverImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.bottom.equalToSuperview() + make.width.equalTo(74) + } + + nameLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(12) + make.top.equalToSuperview().offset(8) + make.right.lessThanOrEqualToSuperview().offset(-60) + } + + categoryLabel.snp.makeConstraints { make in + make.left.equalTo(nameLabel) + make.top.equalTo(nameLabel.snp.bottom).offset(8) + } + + epLabel.snp.makeConstraints { make in + make.left.equalTo(nameLabel) + make.bottom.equalToSuperview().offset(-14) + } + + collectButton.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.centerX.equalTo(self.contentView.snp.right).offset(-31) + } + } + +} diff --git a/XSeri/Class/MyList/View/XSMyListHistoryView.swift b/XSeri/Class/MyList/View/XSMyListHistoryView.swift new file mode 100644 index 0000000..df3c74d --- /dev/null +++ b/XSeri/Class/MyList/View/XSMyListHistoryView.swift @@ -0,0 +1,129 @@ +// +// XSMyListHistoryView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/13. +// + +import UIKit +import SnapKit + +class XSMyListHistoryView: UIView { + + override var intrinsicContentSize: CGSize { + return .init(width: XSScreen.width, height: XSScreen.height) + } + + var dataArr: [XSShortModel] = [] { + didSet { + self.collectionView.reloadData() + } + } + + var refreshHeaderBlock: (() -> Void)? + var refreshFooterBlock: (() -> Void)? + + private lazy var bgView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "bg_image_03")) + imageView.layer.cornerRadius = 20 + imageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + imageView.layer.masksToBounds = true + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 18, weight: .semibold) + label.textColor = .white + label.text = "History".localized + return label + }() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: XSScreen.width, height: 98) + layout.minimumInteritemSpacing = 12 + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.contentInset = .init(top: 13, left: 0, bottom: XSScreen.customTabBarHeight + 10, right: 0) + collectionView.xs_addRefreshHeader(insetTop: collectionView.contentInset.top) { [weak self] in + self?.refreshHeaderBlock?() + } + collectionView.xs_addRefreshFooter(insetBottom: 10) { [weak self] in + self?.refreshFooterBlock?() + } + + collectionView.register(XSMyListHistoryCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func endHeaderRefreshing() { + self.collectionView.xs_endHeaderRefreshing() + } + + func endFooterRefreshing() { + self.collectionView.xs_endFooterRefreshing() + } + +} + +extension XSMyListHistoryView { + + private func xs_setupUI() { + addSubview(bgView) + addSubview(titleLabel) + addSubview(collectionView) + + bgView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(15) + make.top.equalToSuperview().offset(15) + } + + collectionView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.bottom.equalToSuperview() + make.top.equalToSuperview().offset(40) + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSMyListHistoryView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSMyListHistoryCell + cell.model = self.dataArr[indexPath.row] + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.dataArr.count + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = self.dataArr[indexPath.row] + + let vc = XSShortDetailViewController() + vc.shortId = model.short_play_id + self.viewController?.navigationController?.pushViewController(vc, animated: true) + } +} diff --git a/XSeri/Class/Player/Controller/XSShortDetailViewController.swift b/XSeri/Class/Player/Controller/XSShortDetailViewController.swift new file mode 100644 index 0000000..c4a4e3f --- /dev/null +++ b/XSeri/Class/Player/Controller/XSShortDetailViewController.swift @@ -0,0 +1,150 @@ +// +// XSShortDetailViewController.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import JXPlayer +import SnapKit +import FDFullscreenPopGesture + +class XSShortDetailViewController: JXPlayerListViewController { + + override var contentSize: CGSize { + return .init(width: XSScreen.width, height: XSScreen.height) + } + + override var ViewModelClass: JXPlayerListViewModel.Type { + return XSShortDetailViewModel.self + } + + var xs_viewModel: XSShortDetailViewModel { + return self.viewModel as! XSShortDetailViewModel + } + + var shortId: String? { + set { + self.xs_viewModel.shortId = newValue ?? "" + } + get { + return self.xs_viewModel.shortId + } + } + + var activityId: String? + + private lazy var backButton: UIButton = { + let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + self.xs_viewModel.handleBack() + })) + button.setImage(UIImage(named: "arrow_left_icon_01"), for: .normal) + return button + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 18, weight: .bold) + label.textColor = .FFDAA_4 + return label + }() + + override func viewDidLoad() { + super.viewDidLoad() + fd_interactivePopDisabled = true + self.view.backgroundColor = .black + xs_setupUI() + + self.register(XSShortDetailPlayerCell.self, forCellWithReuseIdentifier: "cell") + self.delegate = self + self.dataSource = self + + Task { + await self.xs_viewModel.requestDetailData() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.setNavigationBarHidden(true, animated: true) + } + + override func play() { + super.play() + let videoInfo = self.xs_viewModel.currentCell?.model as? XSVideoInfoModel + + Task { + await XSVideoAPI.requestAddPlayHistory(shortPlayId: videoInfo?.short_play_id, videoId: videoInfo?.short_play_video_id) + } + } + +} + +extension XSShortDetailViewController { + + private func xs_setupUI() { + view.addSubview(backButton) + view.addSubview(titleLabel) + + backButton.snp.makeConstraints { make in + make.top.equalToSuperview().offset(XSScreen.safeTop) + make.height.equalTo(44) + make.width.equalTo(44) + make.left.equalToSuperview().offset(6) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalTo(backButton) + make.left.equalTo(backButton.snp.right).offset(8) + make.right.lessThanOrEqualToSuperview().offset(-16) + } + } + +} + +//AMRK: JXPlayerListViewControllerDataSource +extension XSShortDetailViewController: JXPlayerListViewControllerDataSource { + func jx_playerListViewController(_ viewController: JXPlayerListViewController, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = self.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSShortDetailPlayerCell + cell.model = self.xs_viewModel.dataArr[indexPath.section].episodeList?[indexPath.row] + cell.shortModel = self.xs_viewModel.dataArr[indexPath.section].shortPlayInfo + + let upRow = indexPath.row - 1 + if upRow >= 0, let videoInfo = self.xs_viewModel.dataArr[indexPath.section].episodeList?[upRow], videoInfo.is_lock == true { + cell.hasLastEpisodeUnlocked = true + } else { + cell.hasLastEpisodeUnlocked = false + } + return cell + } + + func jx_playerListViewController(_ viewController: JXPlayerListViewController, numberOfItemsInSection section: Int) -> Int { + return self.xs_viewModel.dataArr[section].episodeList?.count ?? 0 + } + + func jx_numberOfSections(in viewController: JXPlayerListViewController) -> Int { + return self.xs_viewModel.dataArr.count + } + + +} + +//MARK: JXPlayerListViewControllerDelegate +extension XSShortDetailViewController: JXPlayerListViewControllerDelegate { + func jx_shouldAutoScrollNextEpisode(_ viewController: JXPlayerListViewController) -> Bool { + if let _ = self.xs_viewModel.popView { + return false + } else { + return true + } + } + + func jx_playerListViewController(_ viewController: JXPlayerListViewController, didChangeIndexPathForVisible indexPath: IndexPath) { + let model = self.xs_viewModel.dataArr[indexPath.section] + titleLabel.text = model.shortPlayInfo?.name + } +} + + diff --git a/XSeri/Class/Player/Model/XSShortDetailModel.swift b/XSeri/Class/Player/Model/XSShortDetailModel.swift new file mode 100644 index 0000000..6b50785 --- /dev/null +++ b/XSeri/Class/Player/Model/XSShortDetailModel.swift @@ -0,0 +1,20 @@ +// +// XSShortDetailModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import SmartCodable + +class XSShortDetailModel: NSObject, SmartCodable { + + var video_info: XSVideoInfoModel? + var shortPlayInfo: XSShortModel? + var episodeList: [XSVideoInfoModel]? + var is_collect: Bool? + var share_coin: Int? + + required override init() { } +} diff --git a/XSeri/Class/Player/Model/XSShortModel.swift b/XSeri/Class/Player/Model/XSShortModel.swift new file mode 100644 index 0000000..1025203 --- /dev/null +++ b/XSeri/Class/Player/Model/XSShortModel.swift @@ -0,0 +1,46 @@ +// +// XSShortModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import SmartCodable + +class XSShortModel: NSObject, SmartCodable { + + enum TagType: String, SmartCaseDefaultable { + case hot = "hot" + case new = "new" + } + + required override init() { } + + var id: String? + var xs_description: String? + var name: String? + var watch_total: Int? + var current_episode: String? + var image_url: String? + var is_collect: Bool? + var horizontally_img: String? + var categoryList: [XSCategoryModel]? + var collect_total: Int? + var episode_total: Int? + var category: [String]? + var video_url: String? + var short_play_id: String? + var short_play_video_id: String? + var video_info: XSVideoInfoModel? + var tag_type: TagType? + + + + static func mappingForKey() -> [SmartKeyTransformer]? { + return [ + CodingKeys.xs_description <--- ["description", "short_video_description"], + CodingKeys.name <--- ["short_video_title", "name"] + ] + } +} diff --git a/XSeri/Class/Player/Model/XSVideoInfoModel.swift b/XSeri/Class/Player/Model/XSVideoInfoModel.swift new file mode 100644 index 0000000..fc8ee33 --- /dev/null +++ b/XSeri/Class/Player/Model/XSVideoInfoModel.swift @@ -0,0 +1,25 @@ +// +// XSVideoInfoModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import SmartCodable + +class XSVideoInfoModel: NSObject, SmartCodable { + + required override init() { } + + var short_play_id: String? + var video_url: String? + var episode: String? + var coins: Int? + var short_play_video_id: String? + ///是否锁定,购买后解锁 + var is_lock: Bool? + var image_url: String? + ///播放进度,毫秒 + var play_seconds: Int? +} diff --git a/XSeri/Class/Player/View/XSEpSelectorCell.swift b/XSeri/Class/Player/View/XSEpSelectorCell.swift new file mode 100644 index 0000000..7c90240 --- /dev/null +++ b/XSeri/Class/Player/View/XSEpSelectorCell.swift @@ -0,0 +1,86 @@ +// +// XSEpSelectorCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/27. +// + +import UIKit +import SnapKit + +class XSEpSelectorCell: UICollectionViewCell { + + + var model: XSVideoInfoModel? { + didSet { + epLabel.text = model?.episode + } + } + + var xs_isSelected: Bool = false { + didSet { + if xs_isSelected { + bgView.layer.borderColor = UIColor.FFDAA_4.cgColor + bgView.xs_colors = [UIColor._16051_F.cgColor, UIColor._271924.cgColor, UIColor._41382_C.cgColor] + } else { + bgView.layer.borderColor = UIColor.clear.cgColor + bgView.xs_colors = [UIColor.FFD_796.withAlphaComponent(0.15).cgColor, UIColor.FFD_796.withAlphaComponent(0.15).cgColor, UIColor.FFD_796.withAlphaComponent(0.15).cgColor] + } + playImageView.isHidden = !xs_isSelected + } + } + + private lazy var bgView: XSView = { + let view = XSView() + view.layer.cornerRadius = 8 + view.layer.masksToBounds = true + view.layer.borderWidth = 1 + view.layer.borderColor = UIColor.clear.cgColor + view.xs_startPoint = .init(x: 0.5, y: 0) + view.xs_endPoint = .init(x: 0.5, y: 1) +// view.backgroundColor = .FFD_796.withAlphaComponent(0.15) + return view + }() + + private lazy var epLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 16, weight: .bold) + label.textColor = .white + return label + }() + + private lazy var playImageView = UIImageView(image: UIImage(named: "play_icon_02")) + + override init(frame: CGRect) { + super.init(frame: frame) + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSEpSelectorCell { + + private func xs_setupUI() { + contentView.addSubview(bgView) + contentView.addSubview(epLabel) + contentView.addSubview(playImageView) + + bgView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + epLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + playImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(6) + make.bottom.equalToSuperview().offset(-5) + } + } + +} diff --git a/XSeri/Class/Player/View/XSEpSelectorView.swift b/XSeri/Class/Player/View/XSEpSelectorView.swift new file mode 100644 index 0000000..64c4fa6 --- /dev/null +++ b/XSeri/Class/Player/View/XSEpSelectorView.swift @@ -0,0 +1,238 @@ +// +// XSEpSelectorView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/26. +// + +import UIKit +import SnapKit + +class XSEpSelectorView: XSPanModalContentView { + + var selectedIndex: Int = 0 { + didSet { + self.collectionView.reloadData() + } + } + + var model: XSShortDetailModel? { + didSet { + let shortModel = model?.shortPlayInfo + + coverImageView.xs_setImage(shortModel?.image_url) + nameLabel.text = shortModel?.name + desLabel.text = shortModel?.xs_description + + if let category = shortModel?.category?.first, !category.isEmpty { + categoryLabel.text = category + categoryView.isHidden = false + } else { + categoryView.isHidden = true + } + self.collectionView.reloadData() + } + } + + var didSelected: ((_ index: Int) -> Void)? + + private lazy var bgImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "ep_bg_image_01")) + imageView.layer.cornerRadius = 18 + imageView.layer.masksToBounds = true + imageView.layer.borderWidth = 1 + imageView.layer.borderColor = UIColor.FFDAA_4.withAlphaComponent(0.7).cgColor + return imageView + }() + + private lazy var coverImageView: XSImageView = { + let imageView = XSImageView() + imageView.layer.cornerRadius = 4 + imageView.layer.borderWidth = 1 + imageView.layer.borderColor = UIColor.white.withAlphaComponent(0.25).cgColor + return imageView + }() + + private lazy var nameLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .FFDAA_4 + return label + }() + + private lazy var desLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 12, weight: .regular) + label.textColor = .white.withAlphaComponent(0.5) + label.numberOfLines = 2 + return label + }() + + private lazy var categoryView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.5) + view.layer.cornerRadius = 4 + view.layer.masksToBounds = true + return view + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 10, weight: .regular) + label.textColor = .FFDAA_4 + return label + }() + + private lazy var lineView: UIView = { + let view = UIView() + view.backgroundColor = .FFDAA_4.withAlphaComponent(0.25) + return view + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .regular) + label.textColor = .white + label.text = "Episodes".localized + return label + }() + + private lazy var collectionViewLayout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.itemSize = .init(width: 48, height: 48) + layout.minimumInteritemSpacing = 11 + layout.minimumLineSpacing = 11 + return layout + }() + + private lazy var collectionView: XSCollectionView = { + let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.delegate = self + collectionView.dataSource = self + collectionView.showsVerticalScrollIndicator = false + collectionView.contentInset = .init(top: 0, left: 16, bottom: XSScreen.safeBottom + 10, right: 16) + collectionView.register(XSEpSelectorCell.self, forCellWithReuseIdentifier: "cell") + return collectionView + }() + + override init(frame: CGRect) { + super.init(frame: frame) +// self.contentHeight = XSScreen.height / 2 + + self.backgroundColor = .clear + + xs_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func cornerRadius() -> CGFloat { + return 0 + } +} + +extension XSEpSelectorView { + + private func xs_setupUI() { + addSubview(bgImageView) + addSubview(coverImageView) + addSubview(nameLabel) + addSubview(desLabel) + addSubview(categoryView) + categoryView.addSubview(categoryLabel) + addSubview(lineView) + addSubview(titleLabel) + addSubview(collectionView) + + bgImageView.snp.makeConstraints { make in + make.left.right.top.equalToSuperview() + make.bottom.equalToSuperview().offset(9) + } + + coverImageView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.top.equalToSuperview().offset(18) + make.width.equalTo(64) + make.height.equalTo(86) + } + + nameLabel.snp.makeConstraints { make in + make.left.equalTo(coverImageView.snp.right).offset(23) + make.top.equalToSuperview().offset(19) + make.right.lessThanOrEqualToSuperview().offset(-50) + } + + desLabel.snp.makeConstraints { make in + make.left.equalTo(nameLabel) + make.top.equalTo(nameLabel.snp.bottom).offset(6) + make.right.lessThanOrEqualToSuperview().offset(-60) + } + + categoryView.snp.makeConstraints { make in + make.left.equalTo(nameLabel) + make.bottom.equalTo(coverImageView).offset(-5) + make.height.equalTo(14) + } + + categoryLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + make.left.equalToSuperview().offset(6) + } + + lineView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.centerX.equalToSuperview() + make.top.equalTo(coverImageView.snp.bottom).offset(14) + make.height.equalTo(1) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.top.equalTo(lineView.snp.bottom).offset(6) + } + + collectionView.snp.makeConstraints { make in + make.left.right.bottom.equalToSuperview() + make.top.equalTo(lineView.snp.bottom).offset(36) + } + } + +} + +//MARK: UICollectionViewDelegate UICollectionViewDataSource +extension XSEpSelectorView: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSEpSelectorCell + cell.model = self.model?.episodeList?[indexPath.row] + cell.xs_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) { + guard let epList = self.model?.episodeList else { return } + if self.selectedIndex == indexPath.row { return } + +// let lastIndex = indexPath.row - 1 +// var lastIsLock = false +// if lastIndex > 0 && lastIndex < epList.count { +// let lastModel = epList[lastIndex] +// lastIsLock = lastModel.is_lock ?? false +// } +// +// if lastIsLock { +// XSToast.show("buy_fail_toast_02".localized) +// return +// } + + self.dismiss(animated: true) { [weak self] in + self?.didSelected?(indexPath.row) + } + } +} diff --git a/XSeri/Class/Player/View/XSPlayerEpButton.swift b/XSeri/Class/Player/View/XSPlayerEpButton.swift new file mode 100644 index 0000000..2566541 --- /dev/null +++ b/XSeri/Class/Player/View/XSPlayerEpButton.swift @@ -0,0 +1,96 @@ +// +// XSPlayerEpButton.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import SnapKit + +class XSPlayerEpButton: UIControl { + + override var intrinsicContentSize: CGSize { + return .init(width: XSScreen.width - 32, height: 36) + } + + var currentEp: String = "" { + didSet { + currentEpLabel.text = "EP.##".localizedReplace(text: "\(currentEp)") + } + } + + var totalEp: String = "" { + didSet { + totalEpLabel.text = "/ " + "EP.##".localizedReplace(text: "\(totalEp)") + } + } + + private lazy var iconImageView = UIImageView(image: UIImage(named: "list_icon_01")) + + private lazy var currentEpLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .bold) + label.textColor = .FFDAA_4 + return label + }() + + private lazy var totalEpLabel: UILabel = { + let label = UILabel() + label.font = .font(ofSize: 14, weight: .regular) + label.textColor = .white + return label + }() + + private lazy var moreIconImageView = UIImageView(image: UIImage(named: "arrow_up_icon_01")) + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = ._1_F_0_B_00.withAlphaComponent(0.75) + self.layer.cornerRadius = 6 + self.layer.masksToBounds = true + self.layer.borderWidth = 1 + self.layer.borderColor = UIColor.FFDAA_4.withAlphaComponent(0.25).cgColor + + currentEpLabel.text = "EP.1" + totalEpLabel.text = "/ EP.51" + + xs_setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension XSPlayerEpButton { + + private func xs_setupUI() { + addSubview(iconImageView) + addSubview(currentEpLabel) + addSubview(totalEpLabel) + addSubview(moreIconImageView) + + iconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(12) + } + + currentEpLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(iconImageView.snp.right).offset(12) + } + + totalEpLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalTo(currentEpLabel.snp.right).offset(4) + } + + moreIconImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.right.equalToSuperview().offset(-12) + } + } + +} diff --git a/XSeri/Class/Player/View/XSShortDetailPlayerCell.swift b/XSeri/Class/Player/View/XSShortDetailPlayerCell.swift new file mode 100644 index 0000000..773e34a --- /dev/null +++ b/XSeri/Class/Player/View/XSShortDetailPlayerCell.swift @@ -0,0 +1,45 @@ +// +// XSShortDetailPlayerCell.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import JXPlayer + +class XSShortDetailPlayerCell: JXPlayerListCell { + + + override var ControlViewClass: JXPlayerListControlView.Type { + return XSShortDetailPlayerControlView.self + } + + override var model: Any? { + didSet { + let model = self.model as? XSVideoInfoModel + self.player.setPlayUrl(url: model?.video_url ?? "") + +// self.lockView.isHidden = !(model?.is_lock ?? true) +// lockView.videoInfo = model + } + } + + var shortModel: XSShortModel? { + didSet { + xs_controlView.shortModel = shortModel + + self.player.coverImageView?.xs_setImage(shortModel?.image_url) + } + } + + var hasLastEpisodeUnlocked: Bool = false { + didSet { +// self.lockView.hasLastEpisodeUnlocked = hasLastEpisodeUnlocked + } + } + + var xs_controlView: XSShortDetailPlayerControlView { + return self.controlView as! XSShortDetailPlayerControlView + } +} diff --git a/XSeri/Class/Player/View/XSShortDetailPlayerControlView.swift b/XSeri/Class/Player/View/XSShortDetailPlayerControlView.swift new file mode 100644 index 0000000..a4c6b7d --- /dev/null +++ b/XSeri/Class/Player/View/XSShortDetailPlayerControlView.swift @@ -0,0 +1,203 @@ +// +// XSShortDetailPlayerControlView.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import JXPlayer + +class XSShortDetailPlayerControlView: JXPlayerListControlView { + + override var model: Any? { + didSet { + let model = self.model as? XSVideoInfoModel + + epView.currentEp = model?.episode ?? "" + + collectButton.setNeedsUpdateConfiguration() + } + } + + var shortModel: XSShortModel? { + didSet { + epView.totalEp = "\(shortModel?.episode_total ?? 0)" + + } + } + + override var viewModel: JXPlayerListViewModel? { + didSet { + self.viewModel?.addObserver(self, forKeyPath: "isPlaying", context: nil) + } + } + + var xs_viewModel: XSShortDetailViewModel? { + return self.viewModel as? XSShortDetailViewModel + } + + override var durationTime: TimeInterval { + didSet { + updateProgress() + } + } + + override var currentTime: TimeInterval { + didSet { + updateProgress() + } + } + + override var isCurrent: Bool { + didSet { + updatePlayIconState() + } + } + + private lazy var progressView: XSProgressView = { + let view = XSProgressView() + view.insets = .init(top: 10, left: 16, bottom: 10, right: 16) + view.panFinish = { [weak self] progress in + guard let self = self else { return } + self.viewModel?.seekTo(Float(progress)) + } + return view + }() + + private lazy var epView: XSPlayerEpButton = { + let view = XSPlayerEpButton() + view.addAction(UIAction(handler: { [weak self] _ in + guard let self = self else { return } + self.xs_viewModel?.onEpSelectorView() + }), for: .touchUpInside) + return view + }() + + private lazy var collectButton: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.contentInsets = .zero + configuration.imagePadding = 2 + configuration.imagePlacement = .top + configuration.attributedTitle = AttributedString("Save".localized, attributes: AttributeContainer([ + .font : UIFont.font(ofSize: 12, weight: .bold), + .foregroundColor : UIColor.FFDAA_4 + ])) + + let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in + guard let self = self else { return } + guard let model = self.shortModel else { return } + let videoInfo = self.model as? XSVideoInfoModel + let isCollect = !(model.is_collect ?? false) + Task { + await XSVideoAPI.requestCollectShort(isCollect: isCollect, shortId: model.short_play_id ?? "0", videoId: videoInfo?.short_play_video_id) + } + })) + button.configurationUpdateHandler = { [weak self] button in + guard let self = self else { return } + let videoInfo = self.model as? XSVideoInfoModel + + if self.shortModel?.is_collect == true { + button.configuration?.image = UIImage(named: "collect_icon_01_selected") + } else { + button.configuration?.image = UIImage(named: "collect_icon_01") + } + } + return button + }() + + private lazy var playIconImageView = UIImageView() + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + NotificationCenter.default.addObserver(self, selector: #selector(updateShortCollectStateNotification), name: XSVideoAPI.updateShortCollectStateNotification, object: nil) + + xs_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "isPlaying" { + self.updatePlayIconState() + } + } + + private func updateProgress() { + if durationTime == 0 || currentTime == 0 { + progressView.progress = 0 + return + } + progressView.progress = currentTime / durationTime + } + + override func singleTapEvent() { + super.singleTapEvent() + self.viewModel?.userSwitchPlayAndPause() + } + + private func updatePlayIconState() { + let videoInfo = self.model as? XSVideoInfoModel + + if videoInfo?.is_lock == true { + playIconImageView.isHidden = true + } else { + playIconImageView.isHidden = false + if isCurrent == true, self.viewModel?.isPlaying != true { + playIconImageView.image = UIImage(named: "play_icon_01") + } else { + playIconImageView.image = UIImage(named: "pause_icon_01") + } + } + } + + @objc private func updateShortCollectStateNotification(sender: Notification) { + guard let userInfo = sender.userInfo else { return } + guard let shortPlayId = userInfo["id"] as? String else { return } + guard let state = userInfo["state"] as? Bool else { return } + guard shortPlayId == self.shortModel?.short_play_id else { return } + self.shortModel?.is_collect = state + self.collectButton.setNeedsUpdateConfiguration() + } + +} + +extension XSShortDetailPlayerControlView { + + private func xs_setupUI() { + addSubview(progressView) + addSubview(epView) + addSubview(collectButton) + addSubview(playIconImageView) + + progressView.snp.makeConstraints { make in + make.left.equalToSuperview() + make.right.equalToSuperview() + make.bottom.equalToSuperview().offset(-(XSScreen.safeBottom)) + } + + epView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(16) + make.centerX.equalToSuperview() + make.bottom.equalTo(progressView.snp.top).offset(-2) + } + + collectButton.snp.makeConstraints { make in + make.bottom.equalTo(epView.snp.top).offset(-18) + make.right.equalToSuperview().offset(-16) + } + + playIconImageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview() + } + + } + +} diff --git a/XSeri/Class/Player/ViewModel/XSShortDetailViewModel.swift b/XSeri/Class/Player/ViewModel/XSShortDetailViewModel.swift new file mode 100644 index 0000000..6b1132a --- /dev/null +++ b/XSeri/Class/Player/ViewModel/XSShortDetailViewModel.swift @@ -0,0 +1,97 @@ +// +// XSShortDetailViewModel.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/12. +// + +import UIKit +import YYText +import JXPlayer +import HWPanModal + +class XSShortDetailViewModel: JXPlayerListViewModel { + + private(set) var dataArr: [XSShortDetailModel] = [] + + private(set) var recommandDataArr: [XSShortModel] = [] + ///是否展示推荐数据 + private(set) var isShowRecommand = false + private var recommandTimer: Timer? + + var shortId: String = "" + + ///上一次上报播放时长的节点 + private var lastUploadTime: TimeInterval = 0 + + weak var popView: UIView? + +// private var payDataRequest: FAPayDataRequest? + + @MainActor + func requestDetailData(indexPath: IndexPath? = nil) async -> Int { + isShowRecommand = false + recommandTimer?.invalidate() + recommandTimer = nil + await MainActor.run { + recommandTimer = Timer.scheduledTimer(timeInterval: 6, target: YYTextWeakProxy(target: self), selector: #selector(handleRecommandTimer), userInfo: nil, repeats: false) + } + + let (model, code, msg) = await XSVideoAPI.requestShortDetail(shortPlayId: self.shortId) + + if let model = model { + 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 ?? -1 + } + + @objc private func handleRecommandTimer() { + self.isShowRecommand = true + } + + ///点击返回 + func handleBack() { + self.playerListVC?.navigationController?.popViewController(animated: true) + } +} + + +extension XSShortDetailViewModel { + + ///打开剧集选择 + func onEpSelectorView() { + + let view = XSEpSelectorView() + view.selectedIndex = self.currentIndexPath.row + view.model = self.dataArr[currentIndexPath.section] + view.didSelected = { [weak self] index in + guard let self = self else { return } + self.playerListVC?.scrollToItem(indexPath: IndexPath(row: index, section: currentIndexPath.section), animated: false) + } + view.present(in: nil) + self.popView = view + } + +} diff --git a/XSeri/Libs/DeviceId/XSDeviceId.swift b/XSeri/Libs/DeviceId/XSDeviceId.swift new file mode 100644 index 0000000..9376b9e --- /dev/null +++ b/XSeri/Libs/DeviceId/XSDeviceId.swift @@ -0,0 +1,25 @@ +// +// XSDeviceId.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit + +class XSDeviceId: NSObject { + + static let shared = XSDeviceId() + private let key = "com.xseri.deviceid" + + + lazy var id: String = { + if let savedID = XSKeychain.shared.read(key: key) { + return savedID + } else { + let newID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString + XSKeychain.shared.save(key: key, value: newID) + return newID + } + }() +} diff --git a/XSeri/Libs/DeviceId/XSKeychain.swift b/XSeri/Libs/DeviceId/XSKeychain.swift new file mode 100644 index 0000000..bbede31 --- /dev/null +++ b/XSeri/Libs/DeviceId/XSKeychain.swift @@ -0,0 +1,59 @@ +// +// XSKeychain.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit + +class XSKeychain: NSObject { + + static let shared = XSKeychain() + + 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) + } +} diff --git a/XSeri/Libs/Empty/XSEmpty.swift b/XSeri/Libs/Empty/XSEmpty.swift new file mode 100644 index 0000000..d6d2154 --- /dev/null +++ b/XSeri/Libs/Empty/XSEmpty.swift @@ -0,0 +1,27 @@ +// +// XSEmpty.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/2/3. +// + +import UIKit +import LYEmptyView + +struct XSEmpty { + + static func xs_emptyView(image: UIImage? = UIImage(named: "empty_image_01"), title: String? = "empty_title_01".localized) -> LYEmptyView { + + let view = LYEmptyView.emptyActionView(with: image, titleStr: title, detailStr: nil, btnTitleStr: nil) { +// btnClickBlock?() + } + + view?.titleLabFont = .font(ofSize: 14, weight: .medium) + view?.titleLabTextColor = .white.withAlphaComponent(0.6) + view?.contentViewOffset = -100 + view?.subViewMargin = 5 + return view! + + } + +} diff --git a/XSeri/Libs/HUD/XSHud.swift b/XSeri/Libs/HUD/XSHud.swift new file mode 100644 index 0000000..f0db41f --- /dev/null +++ b/XSeri/Libs/HUD/XSHud.swift @@ -0,0 +1,22 @@ +// +// XSHud.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import SVProgressHUD + +struct XSHud { + + static func show(containerView: UIView? = nil, type: SVProgressHUDMaskType = .clear) { + SVProgressHUD.setContainerView(containerView) + SVProgressHUD.setDefaultMaskType(type) + SVProgressHUD.show() + } + + static func dismiss() { + SVProgressHUD.dismiss() + } +} diff --git a/XSeri/Libs/HUD/XSToast.swift b/XSeri/Libs/HUD/XSToast.swift new file mode 100644 index 0000000..0391f91 --- /dev/null +++ b/XSeri/Libs/HUD/XSToast.swift @@ -0,0 +1,24 @@ +// +// XSToast.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import Toast + +struct XSToast { + + static func config() { + CSToastManager.setTapToDismissEnabled(false) + CSToastManager.setDefaultDuration(2) + CSToastManager.setDefaultPosition(CSToastPositionCenter) + } + + static func show(_ text: String?) { + guard let text = text else { return } + XSTool.keyWindow?.makeToast(text) + } + +} diff --git a/XSeri/Libs/Login/XSLoginManager.swift b/XSeri/Libs/Login/XSLoginManager.swift new file mode 100644 index 0000000..05aaeff --- /dev/null +++ b/XSeri/Libs/Login/XSLoginManager.swift @@ -0,0 +1,40 @@ +// +// XSLoginManager.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit + +class XSLoginManager: NSObject { + + static let manager = XSLoginManager() + + private(set) var token = UserDefaults.xs_object(forKey: kXSLoginTokenDefaultsKey, as: XSLoginToken.self) + private(set) var userInfo = UserDefaults.xs_object(forKey: kXSUserInfoDefaultsKey, as: XSUserInfo.self) + + func setAccountToken(_ token: XSLoginToken?) { + self.token = token + UserDefaults.xs_setObject(token, forKey: kXSLoginTokenDefaultsKey) + } + + + func updateUserInfo() async { + guard let userInfo = await XSUserAPI.requestUserInfo() else { return } + + self.userInfo = userInfo + UserDefaults.xs_setObject(userInfo, forKey: kXSUserInfoDefaultsKey) + await MainActor.run { + NotificationCenter.default.post(name: XSLoginManager.userInfoUpdateNotification, object: nil) + } + } +} + +extension XSLoginManager { + + ///用户信息更新 + @objc static let userInfoUpdateNotification = Notification.Name(rawValue: "XSLoginManager.userInfoUpdateNotification") + ///登录状态发生变化 + @objc static let loginStateDidChangeNotification = Notification.Name(rawValue: "XSLoginManager.loginStateDidChangeNotification") +} diff --git a/XSeri/Libs/Login/XSLoginToken.swift b/XSeri/Libs/Login/XSLoginToken.swift new file mode 100644 index 0000000..3507036 --- /dev/null +++ b/XSeri/Libs/Login/XSLoginToken.swift @@ -0,0 +1,38 @@ +// +// XSLoginToken.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import SmartCodable + +class XSLoginToken: NSObject, SmartCodable, NSSecureCoding { + required override init() { } + + var token: String? + var auto_login: Int? + var customer_id: String? + + + static var supportsSecureCoding: Bool { + get { + return true + } + } + + func encode(with coder: NSCoder) { + coder.encode(token, forKey: "token") + coder.encode(auto_login, forKey: "auto_login") + coder.encode(customer_id, forKey: "customer_id") + } + + required init?(coder: NSCoder) { + super.init() + + token = coder.decodeObject(of: NSString.self, forKey: "token") as? String + auto_login = coder.decodeObject(of: NSNumber.self, forKey: "auto_login")?.intValue + customer_id = coder.decodeObject(of: NSString.self, forKey: "customer_id") as? String + } +} diff --git a/XSeri/Libs/Login/XSUserInfo.swift b/XSeri/Libs/Login/XSUserInfo.swift new file mode 100644 index 0000000..475d93a --- /dev/null +++ b/XSeri/Libs/Login/XSUserInfo.swift @@ -0,0 +1,64 @@ +// +// XSUserInfo.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2026/1/4. +// + +import UIKit +import SmartCodable + +class XSUserInfo: NSObject, SmartCodable, NSSecureCoding { + required override init() { } + + var id: String? + var family_name: String? + var send_coin_left_total: Int? + var avator: String? + var coin_left_total: Int? + var vip_end_time: TimeInterval? + var is_vip: Bool? + var is_tourist: Bool? + var customer_id: String? + + 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(family_name, forKey: "family_name") + coder.encode(send_coin_left_total, forKey: "send_coin_left_total") + coder.encode(avator, forKey: "avator") + coder.encode(coin_left_total, forKey: "coin_left_total") + coder.encode(vip_end_time, forKey: "vip_end_time") + coder.encode(is_vip, forKey: "is_vip") + coder.encode(is_tourist, forKey: "is_tourist") + coder.encode(customer_id, forKey: "customer_id") + } + + required init?(coder: NSCoder) { + super.init() + id = coder.decodeObject(of: NSString.self, forKey: "id") as? String + family_name = coder.decodeObject(of: NSString.self, forKey: "family_name") as? String + send_coin_left_total = coder.decodeObject(of: NSNumber.self, forKey: "send_coin_left_total")?.intValue + avator = coder.decodeObject(of: NSString.self, forKey: "avator") as? String + coin_left_total = coder.decodeObject(of: NSNumber.self, forKey: "coin_left_total")?.intValue + vip_end_time = coder.decodeObject(of: NSNumber.self, forKey: "vip_end_time")?.doubleValue + is_vip = coder.decodeObject(of: NSNumber.self, forKey: "is_vip")?.boolValue + is_tourist = coder.decodeObject(of: NSNumber.self, forKey: "is_tourist")?.boolValue + customer_id = coder.decodeObject(of: NSString.self, forKey: "customer_id") as? String + } +} diff --git a/XSeri/Libs/XSTool.swift b/XSeri/Libs/XSTool.swift new file mode 100644 index 0000000..01a75d0 --- /dev/null +++ b/XSeri/Libs/XSTool.swift @@ -0,0 +1,46 @@ +// +// XSTool.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +struct XSTool { + + static var sceneDelegate: SceneDelegate? + + static var windowScene: UIWindowScene? + + static var keyWindow: UIWindow? { + return windowScene?.keyWindow + } + + static var rootViewController: UIViewController? { + return keyWindow?.rootViewController + } + + 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 + } + } +} diff --git a/XSeri/Libs/XSWaterfallFlowLayout.swift b/XSeri/Libs/XSWaterfallFlowLayout.swift new file mode 100644 index 0000000..03a88f9 --- /dev/null +++ b/XSeri/Libs/XSWaterfallFlowLayout.swift @@ -0,0 +1,223 @@ +// +// XSWaterfallFlowLayout.swift +// XSeri +// +// Created by 长沙鸿瑶 on 2025/12/30. +// + +import UIKit + +protocol XSWaterfallMutiSectionDelegate: NSObjectProtocol { + // 必选delegate实现 + /// collectionItem高度 + func heightForRowAtIndexPath(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat + + // 可选delegate实现 + /// 每个section 列数(默认2列) + func columnNumber(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> Int + + /// header高度(默认为0) + func referenceSizeForHeader(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGSize + + /// footer高度(默认为0) + func referenceSizeForFooter(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGSize + + /// 每个section 边距(默认为0) + func insetForSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> UIEdgeInsets + + /// 每个section item上下间距(默认为0) + func lineSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat + + /// 每个section item左右间距(默认为0) + func interitemSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat + + /// section头部header与上个section尾部footer间距(默认为0) + func spacingWithLastSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat +} + +extension XSWaterfallMutiSectionDelegate { + func columnNumber(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> Int { + return 2 + } + + func referenceSizeForHeader(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGSize { + return .zero + } + + func referenceSizeForFooter(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGSize { + return .zero + } + + func insetForSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> UIEdgeInsets { + return .zero + } + + func lineSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat { + return 0 + } + + func interitemSpacing(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat { + return 0 + } + + func spacingWithLastSection(collectionView collection: UICollectionView, layout: XSWaterfallFlowLayout, section: Int) -> CGFloat { + return 0 + } +} + + +class XSWaterfallFlowLayout: UICollectionViewFlowLayout { + weak var delegate: XSWaterfallMutiSectionDelegate? + + private var sectionInsets: UIEdgeInsets = .zero + private var columnCount: Int = 2 + private var lineSpacing: CGFloat = 0 + private var interitemSpacing: CGFloat = 0 + private var headerSize: CGSize = .zero + private var footerSize: CGSize = .zero + + //存放attribute的数组 + private var attrsArray: [UICollectionViewLayoutAttributes] = [] + //存放每个section中各个列的最后一个高度 + private var columnHeights: [CGFloat] = [] + //collectionView的Content的高度 + private var contentHeight: CGFloat = 0 + //记录上个section高度最高一列的高度 + private var lastContentHeight: CGFloat = 0 + //每个section的header与上个section的footer距离 + private var spacingWithLastSection: CGFloat = 0 + + + override func prepare() { + super.prepare() + self.contentHeight = 0 + self.lastContentHeight = 0 + self.spacingWithLastSection = 0 + self.lineSpacing = 0 + self.sectionInsets = .zero + self.headerSize = .zero + self.footerSize = .zero + self.columnHeights.removeAll() + self.attrsArray.removeAll() + + let sectionCount = self.collectionView!.numberOfSections + // 遍历section + for idx in 0.. [UICollectionViewLayoutAttributes]? { + return self.attrsArray + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + if let column = self.delegate?.columnNumber(collectionView: self.collectionView!, layout: self, section: indexPath.section) { + self.columnCount = column + } + if let lineSpacing = self.delegate?.lineSpacing(collectionView: self.collectionView!, layout: self, section: indexPath.section) { + self.lineSpacing = lineSpacing + } + if let interitem = self.delegate?.interitemSpacing(collectionView: self.collectionView!, layout: self, section: indexPath.section) { + self.interitemSpacing = interitem + } + + let attri = UICollectionViewLayoutAttributes(forCellWith: indexPath) + let weight = self.collectionView!.frame.size.width + let itemSpacing = CGFloat(self.columnCount - 1) * self.interitemSpacing + let allWeight = weight - self.sectionInsets.left - self.sectionInsets.right - itemSpacing + let cellWeight = allWeight / CGFloat(self.columnCount) + let cellHeight: CGFloat = (self.delegate?.heightForRowAtIndexPath(collectionView: self.collectionView!, layout: self, indexPath: indexPath, itemWidth: cellWeight))! + + var tmpMinColumn = 0 + var minColumnHeight = self.columnHeights[0] + for i in 0.. columnH { + minColumnHeight = columnH + tmpMinColumn = i + } + } + let cellX = self.sectionInsets.left + CGFloat(tmpMinColumn) * (cellWeight + self.interitemSpacing) + var cellY: CGFloat = 0 + cellY = minColumnHeight + if cellY != self.lastContentHeight { + cellY += self.lineSpacing + } + + if self.contentHeight < minColumnHeight { + self.contentHeight = minColumnHeight + } + + attri.frame = CGRect(x: cellX, y: cellY, width: cellWeight, height: cellHeight) + self.columnHeights[tmpMinColumn] = attri.frame.maxY + //取最大的 + for i in 0.. UICollectionViewLayoutAttributes? { + let attri = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath) + if elementKind == UICollectionView.elementKindSectionHeader { + if let headerSize = self.delegate?.referenceSizeForHeader(collectionView: self.collectionView!, layout: self, section: indexPath.section) { + self.headerSize = headerSize + } + self.contentHeight += self.spacingWithLastSection + attri.frame = CGRect(x: 0, y: self.contentHeight, width: self.headerSize.width, height: self.headerSize.height) + self.contentHeight += self.headerSize.height + self.contentHeight += self.sectionInsets.top + } else if elementKind == UICollectionView.elementKindSectionFooter { + if let footerSize = self.delegate?.referenceSizeForFooter(collectionView: self.collectionView!, layout: self, section: indexPath.section) { + self.footerSize = footerSize + } + self.contentHeight += self.sectionInsets.bottom + attri.frame = CGRect(x: 0, y: self.contentHeight, width: self.footerSize.width, height: self.footerSize.height) + self.contentHeight += self.footerSize.height + } + return attri + } + + override var collectionViewContentSize: CGSize { + return CGSize(width: self.collectionView!.frame.size.width, height: self.contentHeight) + } +} + diff --git a/XSeri/Source/Assets.xcassets/AccentColor.colorset/Contents.json b/XSeri/Source/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/AppIcon.appiconset/Contents.json b/XSeri/Source/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9d5020f --- /dev/null +++ b/XSeri/Source/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "app-icon-1024.jpg", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/AppIcon.appiconset/app-icon-1024.jpg b/XSeri/Source/Assets.xcassets/AppIcon.appiconset/app-icon-1024.jpg new file mode 100644 index 0000000..3a82a90 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/AppIcon.appiconset/app-icon-1024.jpg differ diff --git a/XSeri/Source/Assets.xcassets/Color/#060606.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#060606.colorset/Contents.json new file mode 100644 index 0000000..49376e8 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#060606.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x06", + "green" : "0x06", + "red" : "0x06" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#0F0F0F.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#0F0F0F.colorset/Contents.json new file mode 100644 index 0000000..ad2a076 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#0F0F0F.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x0F", + "green" : "0x0F", + "red" : "0x0F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#16051F.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#16051F.colorset/Contents.json new file mode 100644 index 0000000..9bdaa5a --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#16051F.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1F", + "green" : "0x05", + "red" : "0x16" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#1F0B00.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#1F0B00.colorset/Contents.json new file mode 100644 index 0000000..36e4687 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#1F0B00.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x0B", + "red" : "0x1F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#271924.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#271924.colorset/Contents.json new file mode 100644 index 0000000..3bd5a09 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#271924.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x24", + "green" : "0x19", + "red" : "0x27" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#282828.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#282828.colorset/Contents.json new file mode 100644 index 0000000..d7c65c7 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#282828.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x28", + "green" : "0x28", + "red" : "0x28" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#41382C.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#41382C.colorset/Contents.json new file mode 100644 index 0000000..95b44b0 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#41382C.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2C", + "green" : "0x38", + "red" : "0x41" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#6D71E0.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#6D71E0.colorset/Contents.json new file mode 100644 index 0000000..7206f90 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#6D71E0.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE0", + "green" : "0x71", + "red" : "0x6D" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#783902.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#783902.colorset/Contents.json new file mode 100644 index 0000000..6f7e5c0 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#783902.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x02", + "green" : "0x39", + "red" : "0x78" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#A191FF.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#A191FF.colorset/Contents.json new file mode 100644 index 0000000..141771e --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#A191FF.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x91", + "red" : "0xA1" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#B4A0FF.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#B4A0FF.colorset/Contents.json new file mode 100644 index 0000000..6559017 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#B4A0FF.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xA0", + "red" : "0xB4" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#D7BCFF.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#D7BCFF.colorset/Contents.json new file mode 100644 index 0000000..83f912a --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#D7BCFF.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xBC", + "red" : "0xD7" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#D9D9D9.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#D9D9D9.colorset/Contents.json new file mode 100644 index 0000000..78408a9 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#D9D9D9.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD9", + "green" : "0xD9", + "red" : "0xD9" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#E0C6FF.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#E0C6FF.colorset/Contents.json new file mode 100644 index 0000000..d74d403 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#E0C6FF.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xC6", + "red" : "0xE0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#E7CCFF.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#E7CCFF.colorset/Contents.json new file mode 100644 index 0000000..63363e8 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#E7CCFF.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xCC", + "red" : "0xE7" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#F4C783.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#F4C783.colorset/Contents.json new file mode 100644 index 0000000..cb47b71 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#F4C783.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x83", + "green" : "0xC7", + "red" : "0xF4" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#F5BD7E.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#F5BD7E.colorset/Contents.json new file mode 100644 index 0000000..2c7e254 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#F5BD7E.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7E", + "green" : "0xBD", + "red" : "0xF5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FCD68D.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FCD68D.colorset/Contents.json new file mode 100644 index 0000000..cc33158 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FCD68D.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x8D", + "green" : "0xD6", + "red" : "0xFC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FDE7B8.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FDE7B8.colorset/Contents.json new file mode 100644 index 0000000..bf5a95e --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FDE7B8.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xB8", + "green" : "0xE7", + "red" : "0xFD" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FF313B.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FF313B.colorset/Contents.json new file mode 100644 index 0000000..6e3ce87 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FF313B.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3B", + "green" : "0x31", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FFCF99.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FFCF99.colorset/Contents.json new file mode 100644 index 0000000..29f2c04 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FFCF99.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x99", + "green" : "0xCF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FFD796.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FFD796.colorset/Contents.json new file mode 100644 index 0000000..7f8911e --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FFD796.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x96", + "green" : "0xD7", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FFDAA4.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FFDAA4.colorset/Contents.json new file mode 100644 index 0000000..008c399 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FFDAA4.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA4", + "green" : "0xDA", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FFE6B3.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FFE6B3.colorset/Contents.json new file mode 100644 index 0000000..951b907 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FFE6B3.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FFEABC.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FFEABC.colorset/Contents.json new file mode 100644 index 0000000..523edeb --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FFEABC.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xBC", + "green" : "0xEA", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FFEC80.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FFEC80.colorset/Contents.json new file mode 100644 index 0000000..3a25e9f --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FFEC80.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x80", + "green" : "0xEC", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/#FFEFD8.colorset/Contents.json b/XSeri/Source/Assets.xcassets/Color/#FFEFD8.colorset/Contents.json new file mode 100644 index 0000000..ede9e9d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/#FFEFD8.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD8", + "green" : "0xEF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Color/Contents.json b/XSeri/Source/Assets.xcassets/Color/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Color/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Contents.json b/XSeri/Source/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/Contents.json b/XSeri/Source/Assets.xcassets/Image/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Contents.json new file mode 100644 index 0000000..852ab89 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 2171@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 2171@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Group 2171@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Group 2171@2x.png new file mode 100644 index 0000000..0a972ab Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Group 2171@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Group 2171@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Group 2171@3x.png new file mode 100644 index 0000000..fdb4059 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_sel.imageset/Group 2171@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Charity coins@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Charity coins@2x.png new file mode 100644 index 0000000..d4b4d81 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Charity coins@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Charity coins@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Charity coins@3x.png new file mode 100644 index 0000000..a44b9bb Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Charity coins@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Contents.json new file mode 100644 index 0000000..622a85d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_discover_unsel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Charity coins@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Charity coins@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Contents.json new file mode 100644 index 0000000..02a0c4d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Home选中@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Home选中@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Home选中@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Home选中@2x.png new file mode 100644 index 0000000..3f22cef Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Home选中@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Home选中@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Home选中@3x.png new file mode 100644 index 0000000..b3af04b Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_sel.imageset/Home选中@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Charity coins@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Charity coins@2x.png new file mode 100644 index 0000000..97de331 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Charity coins@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Charity coins@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Charity coins@3x.png new file mode 100644 index 0000000..5831801 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Charity coins@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Contents.json new file mode 100644 index 0000000..622a85d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_home_unsel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Charity coins@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Charity coins@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Contents.json new file mode 100644 index 0000000..852ab89 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 2171@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 2171@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Group 2171@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Group 2171@2x.png new file mode 100644 index 0000000..08191fd Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Group 2171@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Group 2171@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Group 2171@3x.png new file mode 100644 index 0000000..c8af82b Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_sel.imageset/Group 2171@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Charity coins@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Charity coins@2x.png new file mode 100644 index 0000000..d90f66e Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Charity coins@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Charity coins@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Charity coins@3x.png new file mode 100644 index 0000000..47c9a6b Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Charity coins@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Contents.json new file mode 100644 index 0000000..622a85d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_list_unsel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Charity coins@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Charity coins@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Contents.json new file mode 100644 index 0000000..852ab89 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 2171@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 2171@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Group 2171@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Group 2171@2x.png new file mode 100644 index 0000000..8935920 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Group 2171@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Group 2171@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Group 2171@3x.png new file mode 100644 index 0000000..ea608ad Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_sel.imageset/Group 2171@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Frame@2x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Frame@2x.png new file mode 100644 index 0000000..3ce532e Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Frame@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Frame@3x.png b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Frame@3x.png new file mode 100644 index 0000000..797819f Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/TabBar/tab_portfolio_unsel.imageset/Frame@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Contents.json new file mode 100644 index 0000000..2bfbfb9 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 750@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 750@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Group 750@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Group 750@2x.png new file mode 100644 index 0000000..45de8c8 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Group 750@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Group 750@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Group 750@3x.png new file mode 100644 index 0000000..4e9b20a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/about_icon_01.imageset/Group 750@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/Contents.json new file mode 100644 index 0000000..50c3586 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "返回@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "返回@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/返回@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/返回@2x.png new file mode 100644 index 0000000..972bd92 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/返回@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/返回@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/返回@3x.png new file mode 100644 index 0000000..aed7b6c Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_01.imageset/返回@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/Contents.json new file mode 100644 index 0000000..a943b9e --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "返回icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "返回icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/返回icon@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/返回icon@2x.png new file mode 100644 index 0000000..264b6da Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/返回icon@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/返回icon@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/返回icon@3x.png new file mode 100644 index 0000000..5dffa46 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_02.imageset/返回icon@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Contents.json new file mode 100644 index 0000000..01c4199 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 1245@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 1245@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Group 1245@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Group 1245@2x.png new file mode 100644 index 0000000..6c2c74b Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Group 1245@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Group 1245@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Group 1245@3x.png new file mode 100644 index 0000000..b66a8df Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_left_icon_03.imageset/Group 1245@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Contents.json new file mode 100644 index 0000000..6a4d508 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Vector@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Vector@2x.png new file mode 100644 index 0000000..71bdb0f Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Vector@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Vector@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Vector@3x.png new file mode 100644 index 0000000..d576387 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_01.imageset/Vector@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/Contents.json new file mode 100644 index 0000000..397060f --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "arrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "arrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/arrow@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/arrow@2x.png new file mode 100644 index 0000000..57e1c9e Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/arrow@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/arrow@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/arrow@3x.png new file mode 100644 index 0000000..6ef8100 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_02.imageset/arrow@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/Contents.json new file mode 100644 index 0000000..49c2d2e --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "箭头-Icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "箭头-Icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/箭头-Icon@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/箭头-Icon@2x.png new file mode 100644 index 0000000..485247f Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/箭头-Icon@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/箭头-Icon@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/箭头-Icon@3x.png new file mode 100644 index 0000000..f6bfebe Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_right_icon_03.imageset/箭头-Icon@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/Contents.json new file mode 100644 index 0000000..397060f --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "arrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "arrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/arrow@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/arrow@2x.png new file mode 100644 index 0000000..330f42a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/arrow@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/arrow@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/arrow@3x.png new file mode 100644 index 0000000..9d38ba8 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/arrow_up_icon_01.imageset/arrow@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Contents.json new file mode 100644 index 0000000..ba8787e --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 2147238307@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 2147238307@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Frame 2147238307@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Frame 2147238307@2x.png new file mode 100644 index 0000000..06832eb Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Frame 2147238307@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Frame 2147238307@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Frame 2147238307@3x.png new file mode 100644 index 0000000..1ae71d7 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/avatar_placeholder_icon_01.imageset/Frame 2147238307@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/Contents.json new file mode 100644 index 0000000..36577cc --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/bg@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/bg@2x.png new file mode 100644 index 0000000..be21d8a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/bg@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/bg@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/bg@3x.png new file mode 100644 index 0000000..fcba622 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_01.imageset/bg@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/Contents.json new file mode 100644 index 0000000..36577cc --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/bg@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/bg@2x.png new file mode 100644 index 0000000..b2a0b15 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/bg@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/bg@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/bg@3x.png new file mode 100644 index 0000000..99e9551 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_02.imageset/bg@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/Contents.json new file mode 100644 index 0000000..9e8be2c --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "历史记录-bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "历史记录-bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/历史记录-bg@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/历史记录-bg@2x.png new file mode 100644 index 0000000..0fb04dc Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/历史记录-bg@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/历史记录-bg@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/历史记录-bg@3x.png new file mode 100644 index 0000000..fdc665a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/bg_image_03.imageset/历史记录-bg@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/Contents.json new file mode 100644 index 0000000..f34cd3c --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "已收藏@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "已收藏@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/已收藏@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/已收藏@2x.png new file mode 100644 index 0000000..38c1c5e Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/已收藏@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/已收藏@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/已收藏@3x.png new file mode 100644 index 0000000..68d0a60 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01.imageset/已收藏@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/Contents.json new file mode 100644 index 0000000..f34cd3c --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "已收藏@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "已收藏@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/已收藏@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/已收藏@2x.png new file mode 100644 index 0000000..72a2af4 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/已收藏@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/已收藏@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/已收藏@3x.png new file mode 100644 index 0000000..88b5a5e Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_01_selected.imageset/已收藏@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Contents.json new file mode 100644 index 0000000..dd0db90 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Favorites@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Favorites@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Favorites@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Favorites@2x.png new file mode 100644 index 0000000..0832f1b Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Favorites@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Favorites@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Favorites@3x.png new file mode 100644 index 0000000..5a4fa52 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02.imageset/Favorites@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Contents.json new file mode 100644 index 0000000..dd0db90 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Favorites@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Favorites@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Favorites@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Favorites@2x.png new file mode 100644 index 0000000..f27f989 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Favorites@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Favorites@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Favorites@3x.png new file mode 100644 index 0000000..26adbee Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collect_icon_02_selected.imageset/Favorites@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/Contents.json new file mode 100644 index 0000000..1fedfa9 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/Contents.json @@ -0,0 +1,50 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "收藏-bg@2x.png", + "idiom" : "universal", + "resizing" : { + "cap-insets" : { + "bottom" : 35, + "left" : 331, + "right" : 136, + "top" : 329 + }, + "center" : { + "height" : 1, + "mode" : "tile", + "width" : 1 + }, + "mode" : "9-part" + }, + "scale" : "2x" + }, + { + "filename" : "收藏-bg@3x.png", + "idiom" : "universal", + "resizing" : { + "cap-insets" : { + "bottom" : 59, + "left" : 493, + "right" : 190, + "top" : 444 + }, + "center" : { + "height" : 1, + "mode" : "tile", + "width" : 1 + }, + "mode" : "9-part" + }, + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/收藏-bg@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/收藏-bg@2x.png new file mode 100644 index 0000000..31592fa Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/收藏-bg@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/收藏-bg@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/收藏-bg@3x.png new file mode 100644 index 0000000..40792ab Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/collects_bg_image_01.imageset/收藏-bg@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Frame@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Frame@2x.png new file mode 100644 index 0000000..1d6b446 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Frame@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Frame@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Frame@3x.png new file mode 100644 index 0000000..ae98085 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_01.imageset/Frame@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Frame@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Frame@2x.png new file mode 100644 index 0000000..38db40d Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Frame@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Frame@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Frame@3x.png new file mode 100644 index 0000000..30c68cf Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/delete_icon_02.imageset/Frame@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Contents.json new file mode 100644 index 0000000..12edbb9 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 378@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 378@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Group 378@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Group 378@2x.png new file mode 100644 index 0000000..0277c4d Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Group 378@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Group 378@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Group 378@3x.png new file mode 100644 index 0000000..7a876ce Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/edit_icon_01.imageset/Group 378@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/Contents.json new file mode 100644 index 0000000..58f0d91 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "image 1694@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "image 1694@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/image 1694@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/image 1694@2x.png new file mode 100644 index 0000000..9b7548d Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/image 1694@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/image 1694@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/image 1694@3x.png new file mode 100644 index 0000000..5377a9b Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/empty_image_01.imageset/image 1694@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/Contents.json new file mode 100644 index 0000000..1ed4ead --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "选集-bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "选集-bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/选集-bg@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/选集-bg@2x.png new file mode 100644 index 0000000..a50d997 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/选集-bg@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/选集-bg@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/选集-bg@3x.png new file mode 100644 index 0000000..58b3b28 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/ep_bg_image_01.imageset/选集-bg@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Contents.json new file mode 100644 index 0000000..52a1fbd --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 2072750534@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 2072750534@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Group 2072750534@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Group 2072750534@2x.png new file mode 100644 index 0000000..88f9832 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Group 2072750534@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Group 2072750534@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Group 2072750534@3x.png new file mode 100644 index 0000000..cf9ca65 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_01.imageset/Group 2072750534@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Contents.json new file mode 100644 index 0000000..6a4d508 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Vector@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Vector@2x.png new file mode 100644 index 0000000..32f77e4 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Vector@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Vector@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Vector@3x.png new file mode 100644 index 0000000..01eb044 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/feedback_icon_02.imageset/Vector@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Contents.json new file mode 100644 index 0000000..cd9eae2 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 346272050@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 346272050@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Rectangle 346272050@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Rectangle 346272050@2x.png new file mode 100644 index 0000000..79d1f79 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Rectangle 346272050@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Rectangle 346272050@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Rectangle 346272050@3x.png new file mode 100644 index 0000000..d9a1ae6 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/gradient_color_image_01.imageset/Rectangle 346272050@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Choose language@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Choose language@2x.png new file mode 100644 index 0000000..be66be0 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Choose language@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Choose language@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Choose language@3x.png new file mode 100644 index 0000000..c3466be Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Choose language@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Contents.json new file mode 100644 index 0000000..8d93a07 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/history_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Choose language@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Choose language@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Contents.json new file mode 100644 index 0000000..6a4d508 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Vector@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Vector@2x.png new file mode 100644 index 0000000..6923e76 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Vector@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Vector@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Vector@3x.png new file mode 100644 index 0000000..8f56c54 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_01.imageset/Vector@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Badge-带文字-hot@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Badge-带文字-hot@2x.png new file mode 100644 index 0000000..a176efc Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Badge-带文字-hot@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Badge-带文字-hot@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Badge-带文字-hot@3x.png new file mode 100644 index 0000000..c8368ca Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Badge-带文字-hot@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Contents.json new file mode 100644 index 0000000..2f1f570 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Badge-带文字-hot@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Badge-带文字-hot@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/Contents.json new file mode 100644 index 0000000..816160d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "播放器-ICON@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "播放器-ICON@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/播放器-ICON@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/播放器-ICON@2x.png new file mode 100644 index 0000000..93c71c7 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/播放器-ICON@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/播放器-ICON@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/播放器-ICON@3x.png new file mode 100644 index 0000000..484e05a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_03.imageset/播放器-ICON@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/Contents.json new file mode 100644 index 0000000..816160d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "播放器-ICON@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "播放器-ICON@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/播放器-ICON@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/播放器-ICON@2x.png new file mode 100644 index 0000000..3064043 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/播放器-ICON@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/播放器-ICON@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/播放器-ICON@3x.png new file mode 100644 index 0000000..dbe55e8 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/hot_icon_04.imageset/播放器-ICON@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/Contents.json new file mode 100644 index 0000000..ad64fdd --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "列表_list-bottom 1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "列表_list-bottom 1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/列表_list-bottom 1@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/列表_list-bottom 1@2x.png new file mode 100644 index 0000000..d8e800f Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/列表_list-bottom 1@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/列表_list-bottom 1@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/列表_list-bottom 1@3x.png new file mode 100644 index 0000000..dd658ea Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/list_icon_01.imageset/列表_list-bottom 1@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/Contents.json new file mode 100644 index 0000000..23aff50 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "XSeri@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "XSeri@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/XSeri@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/XSeri@2x.png new file mode 100644 index 0000000..3e18838 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/XSeri@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/XSeri@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/XSeri@3x.png new file mode 100644 index 0000000..fd0b07a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_01.imageset/XSeri@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/Contents.json new file mode 100644 index 0000000..f252bf8 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "X字母LOGO 1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "X字母LOGO 1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/X字母LOGO 1@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/X字母LOGO 1@2x.png new file mode 100644 index 0000000..1a85417 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/X字母LOGO 1@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/X字母LOGO 1@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/X字母LOGO 1@3x.png new file mode 100644 index 0000000..6936682 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/logo_icon_02.imageset/X字母LOGO 1@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Badge-带文字-new@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Badge-带文字-new@2x.png new file mode 100644 index 0000000..058418c Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Badge-带文字-new@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Badge-带文字-new@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Badge-带文字-new@3x.png new file mode 100644 index 0000000..3c8d2b1 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Badge-带文字-new@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Contents.json new file mode 100644 index 0000000..487325a --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/new_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Badge-带文字-new@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Badge-带文字-new@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/Contents.json new file mode 100644 index 0000000..c34a0c8 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "暂停_pause@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "暂停_pause@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/暂停_pause@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/暂停_pause@2x.png new file mode 100644 index 0000000..90b8693 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/暂停_pause@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/暂停_pause@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/暂停_pause@3x.png new file mode 100644 index 0000000..1ea495c Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/pause_icon_01.imageset/暂停_pause@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/Contents.json new file mode 100644 index 0000000..4bf2da6 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "剧照缺省@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "剧照缺省@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/剧照缺省@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/剧照缺省@2x.png new file mode 100644 index 0000000..f3af96d Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/剧照缺省@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/剧照缺省@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/剧照缺省@3x.png new file mode 100644 index 0000000..ddeee22 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/placeholder_image.imageset/剧照缺省@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/Contents.json new file mode 100644 index 0000000..c9f3912 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "play@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "play@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/play@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/play@2x.png new file mode 100644 index 0000000..ddfd1c6 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/play@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/play@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/play@3x.png new file mode 100644 index 0000000..a0a2a54 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_01.imageset/play@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Frame@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Frame@2x.png new file mode 100644 index 0000000..27f2309 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Frame@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Frame@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Frame@3x.png new file mode 100644 index 0000000..f88dedb Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/play_icon_02.imageset/Frame@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Contents.json new file mode 100644 index 0000000..592cc0f --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 346272046@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 346272046@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Rectangle 346272046@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Rectangle 346272046@2x.png new file mode 100644 index 0000000..3f16ed4 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Rectangle 346272046@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Rectangle 346272046@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Rectangle 346272046@3x.png new file mode 100644 index 0000000..3b3f15a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_bg_image.imageset/Rectangle 346272046@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Contents.json new file mode 100644 index 0000000..04a32e6 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 491@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 491@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Rectangle 491@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Rectangle 491@2x.png new file mode 100644 index 0000000..7cf8282 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Rectangle 491@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Rectangle 491@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Rectangle 491@3x.png new file mode 100644 index 0000000..a196f82 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_01.imageset/Rectangle 491@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Contents.json new file mode 100644 index 0000000..04a32e6 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 491@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 491@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Rectangle 491@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Rectangle 491@2x.png new file mode 100644 index 0000000..5f48e26 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Rectangle 491@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Rectangle 491@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Rectangle 491@3x.png new file mode 100644 index 0000000..bee06b8 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_02.imageset/Rectangle 491@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Contents.json new file mode 100644 index 0000000..04a32e6 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 491@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 491@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Rectangle 491@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Rectangle 491@2x.png new file mode 100644 index 0000000..e070a28 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Rectangle 491@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Rectangle 491@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Rectangle 491@3x.png new file mode 100644 index 0000000..8d2739d Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/rankings_num_bg_03.imageset/Rectangle 491@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Contents.json new file mode 100644 index 0000000..74b6aa5 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 26@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 26@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Frame 26@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Frame 26@2x.png new file mode 100644 index 0000000..b8e88c5 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Frame 26@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Frame 26@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Frame 26@3x.png new file mode 100644 index 0000000..fc2db76 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_01.imageset/Frame 26@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Contents.json new file mode 100644 index 0000000..5c4d3b1 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Frame@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Frame@2x.png new file mode 100644 index 0000000..ab42180 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Frame@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Frame@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Frame@3x.png new file mode 100644 index 0000000..9e7ee00 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/search_icon_02.imageset/Frame@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Contents.json b/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Contents.json new file mode 100644 index 0000000..68eec3d --- /dev/null +++ b/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 748@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 748@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Group 748@2x.png b/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Group 748@2x.png new file mode 100644 index 0000000..7f7e356 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Group 748@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Group 748@3x.png b/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Group 748@3x.png new file mode 100644 index 0000000..5901f4a Binary files /dev/null and b/XSeri/Source/Assets.xcassets/Image/icon/setting_icon_01.imageset/Group 748@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/LaunchScreen/Contents.json b/XSeri/Source/Assets.xcassets/LaunchScreen/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/XSeri/Source/Assets.xcassets/LaunchScreen/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/Contents.json b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/Contents.json new file mode 100644 index 0000000..d99a60c --- /dev/null +++ b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "启动页@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "启动页@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/启动页@2x.png b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/启动页@2x.png new file mode 100644 index 0000000..c082d15 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/启动页@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/启动页@3x.png b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/启动页@3x.png new file mode 100644 index 0000000..4f86758 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_bg_image.imageset/启动页@3x.png differ diff --git a/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/Contents.json b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/Contents.json new file mode 100644 index 0000000..71810bb --- /dev/null +++ b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/logo@2x.png b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/logo@2x.png new file mode 100644 index 0000000..114d436 Binary files /dev/null and b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/logo@2x.png differ diff --git a/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/logo@3x.png b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/logo@3x.png new file mode 100644 index 0000000..9878acc Binary files /dev/null and b/XSeri/Source/Assets.xcassets/LaunchScreen/launch_screen_logo_image.imageset/logo@3x.png differ diff --git a/XSeri/Source/Base.lproj/LaunchScreen.storyboard b/XSeri/Source/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..ff9cd1f --- /dev/null +++ b/XSeri/Source/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XSeri/Source/Info.plist b/XSeri/Source/Info.plist new file mode 100644 index 0000000..2b56941 --- /dev/null +++ b/XSeri/Source/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIDesignRequiresCompatibility + + + diff --git a/XSeri/Source/en.lproj/Localizable.strings b/XSeri/Source/en.lproj/Localizable.strings new file mode 100644 index 0000000..c62901f --- /dev/null +++ b/XSeri/Source/en.lproj/Localizable.strings @@ -0,0 +1,49 @@ +/* + Localizable.strings + XSeri + + Created by 长沙鸿瑶 on 2025/12/30. + +*/ + + +"Home" = "Home"; +"Discover" = "Discover"; +"My List" = "My List"; +"Portfolio" = "Portfolio"; +"Popular" = "Popular"; +"New" = "New"; +"Rankings" = "Rankings"; +"Categories" = "Categories"; +"EP.##" = "EP.##"; +"Save" = "Save"; +"Recent" = "Recent"; +"Hot Searches" = "Hot Searches"; +"Rising Popularity" = "Rising Popularity"; +"Top-Rated Charts" = "Top-Rated Charts"; +"Love in Ashes is he" = "Love in Ashes is he"; +"About" = "About"; +"Privacy Policy" = "Privacy Policy"; +"User Agreement" = "User Agreement"; +"Visit Website" = "Visit Website"; +"My List" = "My List"; +"My Favorites" = "My Favorites"; +"History" = "History"; +"See All" = "See All"; +"Browsing History" = "Browsing History"; +"## Episodes" = "## Episodes"; +"Episodes" = "Episodes"; +"Error" = "Error"; +"feedback_history" = "Feedback History"; +"feedback" = "Feedback"; +"feedback_detail" = "Feedback Details"; +"account_deletion" = "Account Deletion"; + +"empty_title_01" = "We couldn’t find any dramas"; + + +"buy_fail_toast_01" = "Purchase failed, please try again later!"; +"buy_fail_toast_02" = "The prequel to this series is not unlocked. Please unlock the prequel before unlocking this series"; + +"network_error_1" = "Your account is already logged in on another device~"; +"network_error_2" = "The service is abnormal. Check the network.";