diff --git a/crush/.gitignore b/crush/.gitignore deleted file mode 100644 index 173ec03..0000000 --- a/crush/.gitignore +++ /dev/null @@ -1,99 +0,0 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## Build generated -build/ -DerivedData/ - -## Various settings -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata/ - -## Other -*.moved-aside -*.xccheckout -*.xcscmblueprint - -## Obj-C/Swift specific -*.hmap -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -.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 - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ -*/.DS_Store -/EGirl/.DS_Store -.DS_Store -/EGirl/Views/.DS_Store -/Pods - - -# fastlane -/fastlane/report.xml -/fastlane/Preview.html -/fastlane/screenshots -/fastlane/metadata - -/Gemfile.lock -/Gemfile -*.generated.swift - - - - -.vscode diff --git a/crush/Crush.xcodeproj/project.pbxproj b/crush/Crush.xcodeproj/project.pbxproj deleted file mode 100644 index 793bcbc..0000000 --- a/crush/Crush.xcodeproj/project.pbxproj +++ /dev/null @@ -1,866 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXBuildFile section */ - 70C2E7E02E23CC04009FD07A /* CodableWrappers in Frameworks */ = {isa = PBXBuildFile; productRef = 70C2E7DF2E23CC04009FD07A /* CodableWrappers */; }; - 70C2E9B72E23D3EF009FD07A /* URLMatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 70C2E9B62E23D3EF009FD07A /* URLMatcher */; }; - 70C2E9B92E23D3EF009FD07A /* URLNavigator in Frameworks */ = {isa = PBXBuildFile; productRef = 70C2E9B82E23D3EF009FD07A /* URLNavigator */; }; - ECAA6BDA2E979B8800E9895A /* Pods_Crush.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3F28BC56FDB87793B5DD2F5 /* Pods_Crush.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 70D22BD52E21390600A71DEB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 70FCBA472E1CEE8800B29921 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 70FCBA4E2E1CEE8800B29921; - remoteInfo = Crush; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 3B4D74B75E2A2A78834C3F32 /* Pods-Crush.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Crush.debug.xcconfig"; path = "Target Support Files/Pods-Crush/Pods-Crush.debug.xcconfig"; sourceTree = ""; }; - 558F2A2974C89AB7C58D6882 /* Pods-Crush.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Crush.release.xcconfig"; path = "Target Support Files/Pods-Crush/Pods-Crush.release.xcconfig"; sourceTree = ""; }; - 70D22BD12E21390600A71DEB /* CrushTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CrushTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 70FCBA4F2E1CEE8800B29921 /* CrushLevel.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CrushLevel.app; sourceTree = BUILT_PRODUCTS_DIR; }; - B740E3F620D396050428D836 /* Pods-Crush.appstore.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Crush.appstore.xcconfig"; path = "Target Support Files/Pods-Crush/Pods-Crush.appstore.xcconfig"; sourceTree = ""; }; - C3F28BC56FDB87793B5DD2F5 /* Pods_Crush.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Crush.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E1E8BF8E5327610208416027 /* Pods-Crush.product.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Crush.product.xcconfig"; path = "Target Support Files/Pods-Crush/Pods-Crush.product.xcconfig"; sourceTree = ""; }; - ECAA63862E8BEB3900E9895A /* BytePlusRTC.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BytePlusRTC.xcframework; path = Pods/BytePlusRTC/BytePlusRTC.xcframework; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 70D22C432E2144F600A71DEB /* Exceptions for "Crush" folder in "CrushTests" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Src/Components/Base/CLNavigationController.swift, - Src/Modules/TestEntrances/TestEntrancesController.swift, - Src/Utils/Extensions/CommonExt.swift, - ); - target = 70D22BD02E21390600A71DEB /* CrushTests */; - }; - 70FCBA612E1CEE8A00B29921 /* Exceptions for "Crush" folder in "Crush" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 70FCBA4E2E1CEE8800B29921 /* Crush */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 70D22BD22E21390600A71DEB /* CrushTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = CrushTests; - sourceTree = ""; - }; - 70FCBA512E1CEE8800B29921 /* Crush */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 70FCBA612E1CEE8A00B29921 /* Exceptions for "Crush" folder in "Crush" target */, - 70D22C432E2144F600A71DEB /* Exceptions for "Crush" folder in "CrushTests" target */, - ); - path = Crush; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - 70D22BCE2E21390600A71DEB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 70FCBA4C2E1CEE8800B29921 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 70C2E9B72E23D3EF009FD07A /* URLMatcher in Frameworks */, - 70C2E7E02E23CC04009FD07A /* CodableWrappers in Frameworks */, - ECAA6BDA2E979B8800E9895A /* Pods_Crush.framework in Frameworks */, - 70C2E9B92E23D3EF009FD07A /* URLNavigator in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 34DBC4421D6AB48D485892A6 /* Pods */ = { - isa = PBXGroup; - children = ( - 3B4D74B75E2A2A78834C3F32 /* Pods-Crush.debug.xcconfig */, - 558F2A2974C89AB7C58D6882 /* Pods-Crush.release.xcconfig */, - B740E3F620D396050428D836 /* Pods-Crush.appstore.xcconfig */, - E1E8BF8E5327610208416027 /* Pods-Crush.product.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 70FCBA462E1CEE8800B29921 = { - isa = PBXGroup; - children = ( - 70FCBA512E1CEE8800B29921 /* Crush */, - 70D22BD22E21390600A71DEB /* CrushTests */, - 70FCBA502E1CEE8800B29921 /* Products */, - 34DBC4421D6AB48D485892A6 /* Pods */, - DC9D4EE3C3EA00FA653EB4E8 /* Frameworks */, - ); - sourceTree = ""; - }; - 70FCBA502E1CEE8800B29921 /* Products */ = { - isa = PBXGroup; - children = ( - 70FCBA4F2E1CEE8800B29921 /* CrushLevel.app */, - 70D22BD12E21390600A71DEB /* CrushTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - DC9D4EE3C3EA00FA653EB4E8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ECAA63862E8BEB3900E9895A /* BytePlusRTC.xcframework */, - C3F28BC56FDB87793B5DD2F5 /* Pods_Crush.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 70D22BD02E21390600A71DEB /* CrushTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 70D22BD72E21390600A71DEB /* Build configuration list for PBXNativeTarget "CrushTests" */; - buildPhases = ( - 70D22BCD2E21390600A71DEB /* Sources */, - 70D22BCE2E21390600A71DEB /* Frameworks */, - 70D22BCF2E21390600A71DEB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 70D22BD62E21390600A71DEB /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 70D22BD22E21390600A71DEB /* CrushTests */, - ); - name = CrushTests; - productName = CrushTests; - productReference = 70D22BD12E21390600A71DEB /* CrushTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 70FCBA4E2E1CEE8800B29921 /* Crush */ = { - isa = PBXNativeTarget; - buildConfigurationList = 70FCBA622E1CEE8A00B29921 /* Build configuration list for PBXNativeTarget "Crush" */; - buildPhases = ( - 338FAE1161D2BC8FD6A169BE /* [CP] Check Pods Manifest.lock */, - 70FCBA4B2E1CEE8800B29921 /* Sources */, - 70FCBA4C2E1CEE8800B29921 /* Frameworks */, - 70FCBA4D2E1CEE8800B29921 /* Resources */, - B67810F7496EF5CA64FB53CD /* [CP] Embed Pods Frameworks */, - 70D22D6A2E225C2600A71DEB /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 70FCBA512E1CEE8800B29921 /* Crush */, - ); - name = Crush; - productName = Crush; - productReference = 70FCBA4F2E1CEE8800B29921 /* CrushLevel.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 70FCBA472E1CEE8800B29921 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1630; - LastUpgradeCheck = 1630; - TargetAttributes = { - 70D22BD02E21390600A71DEB = { - CreatedOnToolsVersion = 16.3; - TestTargetID = 70FCBA4E2E1CEE8800B29921; - }; - 70FCBA4E2E1CEE8800B29921 = { - CreatedOnToolsVersion = 16.3; - }; - }; - }; - buildConfigurationList = 70FCBA4A2E1CEE8800B29921 /* Build configuration list for PBXProject "Crush" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 70FCBA462E1CEE8800B29921; - minimizedProjectReferenceProxies = 1; - packageReferences = ( - 70C2E7DE2E23CC04009FD07A /* XCRemoteSwiftPackageReference "CodableWrappers" */, - 70C2E9B52E23D3EF009FD07A /* XCRemoteSwiftPackageReference "URLNavigator" */, - ); - preferredProjectObjectVersion = 77; - productRefGroup = 70FCBA502E1CEE8800B29921 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 70FCBA4E2E1CEE8800B29921 /* Crush */, - 70D22BD02E21390600A71DEB /* CrushTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 70D22BCF2E21390600A71DEB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 70FCBA4D2E1CEE8800B29921 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 338FAE1161D2BC8FD6A169BE /* [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-Crush-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; - }; - 70D22D6A2E225C2600A71DEB /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - $SRCROOT/R.generated.swift, - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"$PODS_ROOT/R.swift/rswift\" generate \"$SRCROOT/Crush/R.generated.swift\"\n"; - }; - B67810F7496EF5CA64FB53CD /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Crush/Pods-Crush-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Crush/Pods-Crush-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Crush/Pods-Crush-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 70D22BCD2E21390600A71DEB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 70FCBA4B2E1CEE8800B29921 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 70D22BD62E21390600A71DEB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 70FCBA4E2E1CEE8800B29921 /* Crush */; - targetProxy = 70D22BD52E21390600A71DEB /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 70D22BD82E21390600A71DEB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = gg.eapl.CrushTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Crush.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Crush"; - }; - name = Debug; - }; - 70D22BD92E21390600A71DEB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = gg.eapl.CrushTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Crush.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Crush"; - }; - name = Release; - }; - 70FC185D2E51F1AF0095980F /* Product */ = { - 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"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - 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.4; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Product; - }; - 70FC185E2E51F1AF0095980F /* Product */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = E1E8BF8E5327610208416027 /* Pods-Crush.product.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Crush/Crush.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 121; - DEVELOPMENT_TEAM = 6GS5RC7C89; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "$(SRCROOT)/Crush/PrefixHeader.pch"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Crush/Info.plist; - INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; - INFOPLIST_KEY_NSCameraUsageDescription = "App requires camera permission to take photos and avatars"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "App needs your microphone permission to record audio messages"; - INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "App requires album permission to set in-app photos and avatars"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UIStatusBarHidden = YES; - INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.person.ChinaTravel; - PRODUCT_NAME = CrushLevel; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CLPRODUCT; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Crush/CL-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Product; - }; - 70FC185F2E51F1AF0095980F /* Product */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = gg.eapl.CrushTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Crush.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Crush"; - }; - name = Product; - }; - 70FC18602E51F2180095980F /* AppStore */ = { - 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"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - 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.4; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = AppStore; - }; - 70FC18612E51F2180095980F /* AppStore */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = B740E3F620D396050428D836 /* Pods-Crush.appstore.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Crush/Crush.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 121; - DEVELOPMENT_TEAM = 6GS5RC7C89; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "$(SRCROOT)/Crush/PrefixHeader.pch"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Crush/Info.plist; - INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; - INFOPLIST_KEY_NSCameraUsageDescription = "App requires camera permission to take photos and avatars"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "App needs your microphone permission to record audio messages"; - INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "App requires album permission to set in-app photos and avatars"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UIStatusBarHidden = YES; - INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.person.ChinaTravel; - PRODUCT_NAME = CrushLevel; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = CLAPPSTORE; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Crush/CL-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = AppStore; - }; - 70FC18622E51F2180095980F /* AppStore */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = gg.eapl.CrushTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Crush.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Crush"; - }; - name = AppStore; - }; - 70FCBA632E1CEE8A00B29921 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3B4D74B75E2A2A78834C3F32 /* Pods-Crush.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Crush/Crush.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 121; - DEVELOPMENT_TEAM = 6GS5RC7C89; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "$(SRCROOT)/Crush/PrefixHeader.pch"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Crush/Info.plist; - INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; - INFOPLIST_KEY_NSCameraUsageDescription = "App requires camera permission to take photos and avatars"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "App needs your microphone permission to record audio messages"; - INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "App requires album permission to set in-app photos and avatars"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UIStatusBarHidden = YES; - INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.person.ChinaTravel; - PRODUCT_NAME = CrushLevel; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Crush/CL-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 70FCBA642E1CEE8A00B29921 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 558F2A2974C89AB7C58D6882 /* Pods-Crush.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Crush/Crush.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 121; - DEVELOPMENT_TEAM = 6GS5RC7C89; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "$(SRCROOT)/Crush/PrefixHeader.pch"; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Crush/Info.plist; - INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; - INFOPLIST_KEY_NSCameraUsageDescription = "App requires camera permission to take photos and avatars"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "App needs your microphone permission to record audio messages"; - INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "App requires album permission to set in-app photos and avatars"; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UIStatusBarHidden = YES; - INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.person.ChinaTravel; - PRODUCT_NAME = CrushLevel; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Crush/CL-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 70FCBA652E1CEE8A00B29921 /* 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; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - 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.4; - 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; - }; - 70FCBA662E1CEE8A00B29921 /* 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"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - 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.4; - 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 */ - 70D22BD72E21390600A71DEB /* Build configuration list for PBXNativeTarget "CrushTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 70D22BD82E21390600A71DEB /* Debug */, - 70D22BD92E21390600A71DEB /* Release */, - 70FC18622E51F2180095980F /* AppStore */, - 70FC185F2E51F1AF0095980F /* Product */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 70FCBA4A2E1CEE8800B29921 /* Build configuration list for PBXProject "Crush" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 70FCBA652E1CEE8A00B29921 /* Debug */, - 70FCBA662E1CEE8A00B29921 /* Release */, - 70FC18602E51F2180095980F /* AppStore */, - 70FC185D2E51F1AF0095980F /* Product */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 70FCBA622E1CEE8A00B29921 /* Build configuration list for PBXNativeTarget "Crush" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 70FCBA632E1CEE8A00B29921 /* Debug */, - 70FCBA642E1CEE8A00B29921 /* Release */, - 70FC18612E51F2180095980F /* AppStore */, - 70FC185E2E51F1AF0095980F /* Product */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 70C2E7DE2E23CC04009FD07A /* XCRemoteSwiftPackageReference "CodableWrappers" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/GottaGetSwifty/CodableWrappers.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.1.0; - }; - }; - 70C2E9B52E23D3EF009FD07A /* XCRemoteSwiftPackageReference "URLNavigator" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/devxoul/URLNavigator"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.5.1; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 70C2E7DF2E23CC04009FD07A /* CodableWrappers */ = { - isa = XCSwiftPackageProductDependency; - package = 70C2E7DE2E23CC04009FD07A /* XCRemoteSwiftPackageReference "CodableWrappers" */; - productName = CodableWrappers; - }; - 70C2E9B62E23D3EF009FD07A /* URLMatcher */ = { - isa = XCSwiftPackageProductDependency; - package = 70C2E9B52E23D3EF009FD07A /* XCRemoteSwiftPackageReference "URLNavigator" */; - productName = URLMatcher; - }; - 70C2E9B82E23D3EF009FD07A /* URLNavigator */ = { - isa = XCSwiftPackageProductDependency; - package = 70C2E9B52E23D3EF009FD07A /* XCRemoteSwiftPackageReference "URLNavigator" */; - productName = URLNavigator; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 70FCBA472E1CEE8800B29921 /* Project object */; -} diff --git a/crush/Crush.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/crush/Crush.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/crush/Crush.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/crush/Crush.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/crush/Crush.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 2050af0..0000000 --- a/crush/Crush.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,33 +0,0 @@ -{ - "originHash" : "2f90c79e2a73629eebb24cd43f6b51a98a927f18ea19cda25ebc8a7baa2e4d80", - "pins" : [ - { - "identity" : "codablewrappers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GottaGetSwifty/CodableWrappers.git", - "state" : { - "revision" : "44140bf929cc1de185db824a20c027385a640ce4", - "version" : "3.1.0" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", - "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - }, - { - "identity" : "urlnavigator", - "kind" : "remoteSourceControl", - "location" : "https://github.com/devxoul/URLNavigator", - "state" : { - "revision" : "b881f23dc3b381986ed6cf76da8e426ce5c8a7ee", - "version" : "2.5.1" - } - } - ], - "version" : 3 -} diff --git a/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush.xcscheme b/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush.xcscheme deleted file mode 100644 index 2655abc..0000000 --- a/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush_AppStore.xcscheme b/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush_AppStore.xcscheme deleted file mode 100644 index 484699a..0000000 --- a/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush_AppStore.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush_Product.xcscheme b/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush_Product.xcscheme deleted file mode 100644 index e729eb3..0000000 --- a/crush/Crush.xcodeproj/xcshareddata/xcschemes/Crush_Product.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crush/Crush.xcworkspace/contents.xcworkspacedata b/crush/Crush.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7a67300..0000000 --- a/crush/Crush.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/crush/Crush.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/crush/Crush.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index 0c67376..0000000 --- a/crush/Crush.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/crush/Crush.xcworkspace/xcshareddata/swiftpm/Package.resolved b/crush/Crush.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 2050af0..0000000 --- a/crush/Crush.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,33 +0,0 @@ -{ - "originHash" : "2f90c79e2a73629eebb24cd43f6b51a98a927f18ea19cda25ebc8a7baa2e4d80", - "pins" : [ - { - "identity" : "codablewrappers", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GottaGetSwifty/CodableWrappers.git", - "state" : { - "revision" : "44140bf929cc1de185db824a20c027385a640ce4", - "version" : "3.1.0" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", - "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - }, - { - "identity" : "urlnavigator", - "kind" : "remoteSourceControl", - "location" : "https://github.com/devxoul/URLNavigator", - "state" : { - "revision" : "b881f23dc3b381986ed6cf76da8e426ce5c8a7ee", - "version" : "2.5.1" - } - } - ], - "version" : 3 -} diff --git a/crush/Crush/AppDelegate.swift b/crush/Crush/AppDelegate.swift deleted file mode 100644 index c627866..0000000 --- a/crush/Crush/AppDelegate.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// AppDelegate.swift -// Crush -// -// Created by lyu dong on 2025/7/8. -// - -import UIKit -import URLNavigator -import AWSS3 -import AWSMobileClient - -//let navigator = Navigator() - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - var appInitial: AppLaunchInitial = AppLaunchInitial() -// private var navigator: NavigatorProtocol? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - -// NavigationMap.initialize(navigator: navigator) - - appInitial.setupCommon() - - // Window Set is in SceneDelegate.swift -// DispatchQueue.main.asyncAfter(deadline: .now() + 5) { -// navigator.open("crushlevel://aichat/443040313704449") -// } - - 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. - } - - func application(_ application: UIApplication, - handleEventsForBackgroundURLSession identifier: String, - completionHandler: @escaping () -> Void){ - AWSMobileClient.default().initialize { (userState, error) in - guard error == nil else { - dlog("❌Error initializing AWSMobileClient. Error: \(error!.localizedDescription)") - return - } - dlog("✅AWSMobileClient initialized.") - } - - //provide the completionHandler to the TransferUtility to support background transfers. - AWSS3TransferUtility.interceptApplication(application, - handleEventsForBackgroundURLSession: identifier, - completionHandler: completionHandler) - } - - func application( - _ app: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey: Any] = [:] - ) -> Bool { -// // Try presenting the URL first -// if navigator.present(url, wrap: UINavigationController.self) != nil { -// print("[Navigator] present: \(url)") -// return true -// } - -// // Try opening the URL -// if navigator.open(url) == true { -// print("[Navigator] open: \(url)") -// return true -// } - - return false - } -} diff --git a/crush/Crush/AppLaunchInitial.swift b/crush/Crush/AppLaunchInitial.swift deleted file mode 100644 index e1656e7..0000000 --- a/crush/Crush/AppLaunchInitial.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AppLaunchInitial.swift -// Crush -// -// Created by Leon on 2025/7/19. -// -import Foundation -//import IQKeyboardManagerSwift -import IQKeyboardToolbarManager -import IQKeyboardManagerSwift -class AppLaunchInitial{ - - public func setupCommon(){ - // User - UserCore.shared.autoLoginTry() - - setupUIAbout() - - LTNetworkManage.ltManage.netWorkReachability(reachabilityStatus: nil) - - IAPCore.shared.check() - - IMManager.shared.setupNIM() - - H5BaseViewController.clearCache() - - loadApis(excludeNoNeedLogin: false) - - setupEvent() - } - - /// excludeNoNeedLogin, 排除掉不需要登陆的。 一般使用false - private func loadApis(excludeNoNeedLogin : Bool){ - // Req Dict Data - if excludeNoNeedLogin == false{ - AppDictManager.shared.loadRequiredDict() - } - - WalletCore.shared.refreshWallet(block: nil) - IMManager.shared.regetNoticeUnread() - } - - private func setupEvent(){ - NotificationCenter.default.addObserver(self, selector: #selector(notifyNetworkRestore), name: AppNotificationName.networkRestored.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(loginIn), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - } - - - // MARK: - Functions - private func setupUIAbout(){ - setupIQKeyboard() - - CLPopoverListView.setupCLStyle() - - UICommonSetup.setupCLStyle() - } - - // MARK: Helper - private func setupIQKeyboard(){ - DispatchQueue.main.async { - IQKeyboardToolbarManager.shared.isEnabled = true - IQKeyboardToolbarManager.shared.toolbarConfiguration.tintColor = .c.cpn - IQKeyboardManager.shared.isEnabled = true - - IQKeyboardManager.shared.disabledDistanceHandlingClasses = [SessionController.self] - } - } - - // MARK: - Notification - - @objc private func notifyNetworkRestore(){ - dlog("🛜网络恢复") - loadApis(excludeNoNeedLogin: false) - } - - @objc private func loginIn(){ - loadApis(excludeNoNeedLogin: true) - } - - - - -} diff --git a/crush/Crush/Assets.xcassets/AccentColor.colorset/Contents.json b/crush/Crush/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/crush/Crush/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/Contents.json b/crush/Crush/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 03770e9..0000000 --- a/crush/Crush/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon_40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon_60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "icon_58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon_87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "icon_80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon_120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "icon_120.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "icon_180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "icon_20.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "icon_40.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon_29.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "icon_58.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon_40.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "icon_80.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon_76.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "filename" : "icon_152.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "icon_167.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "icon_1024.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_1024.png deleted file mode 100644 index e12e1e5..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_1024.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_120.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_120.png deleted file mode 100644 index 9ba7d2a..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_120.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_152.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_152.png deleted file mode 100644 index fa2cd54..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_152.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_167.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_167.png deleted file mode 100644 index 1d218c7..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_167.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_180.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_180.png deleted file mode 100644 index 2cfbd30..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_180.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_20.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_20.png deleted file mode 100644 index a9f826e..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_20.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_29.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_29.png deleted file mode 100644 index bc55cd0..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_29.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_40.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_40.png deleted file mode 100644 index 6117129..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_40.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_58.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_58.png deleted file mode 100644 index c6b86ff..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_58.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_60.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_60.png deleted file mode 100644 index b28d495..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_60.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_76.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_76.png deleted file mode 100644 index ca5027b..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_76.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_80.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_80.png deleted file mode 100644 index ae91fef..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_80.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_87.png b/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_87.png deleted file mode 100644 index 01d627d..0000000 Binary files a/crush/Crush/Assets.xcassets/AppIcon.appiconset/icon_87.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/Contents.json b/crush/Crush/Assets.xcassets/Chat/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Chat/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/Contents.json deleted file mode 100644 index 8d55dda..0000000 --- a/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "ai_phone_call_silent@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "ai_phone_call_silent@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/ai_phone_call_silent@2x.png b/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/ai_phone_call_silent@2x.png deleted file mode 100644 index e8f6429..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/ai_phone_call_silent@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/ai_phone_call_silent@3x.png b/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/ai_phone_call_silent@3x.png deleted file mode 100644 index 2d91487..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/ai_phone_call_silent.imageset/ai_phone_call_silent@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/Contents.json deleted file mode 100644 index 2a78151..0000000 --- a/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "card_swipe_flag@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "card_swipe_flag@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/card_swipe_flag@2x.png b/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/card_swipe_flag@2x.png deleted file mode 100644 index 8f73ac7..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/card_swipe_flag@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/card_swipe_flag@3x.png b/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/card_swipe_flag@3x.png deleted file mode 100644 index a7ec403..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/card_swipe_flag.imageset/card_swipe_flag@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/chat_bubble_default.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/chat_bubble_default.imageset/Contents.json deleted file mode 100644 index 4e06e1f..0000000 --- a/crush/Crush/Assets.xcassets/Chat/chat_bubble_default.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "chat_bubble_default.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/chat_bubble_default.imageset/chat_bubble_default.png b/crush/Crush/Assets.xcassets/Chat/chat_bubble_default.imageset/chat_bubble_default.png deleted file mode 100644 index a695f04..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/chat_bubble_default.imageset/chat_bubble_default.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/Contents.json deleted file mode 100644 index 439c903..0000000 --- a/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "cr_gift_count_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "cr_gift_count_bg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/cr_gift_count_bg@2x.png b/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/cr_gift_count_bg@2x.png deleted file mode 100644 index e43424e..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/cr_gift_count_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/cr_gift_count_bg@3x.png b/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/cr_gift_count_bg@3x.png deleted file mode 100644 index b8fe0c3..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/cr_gift_count_bg.imageset/cr_gift_count_bg@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/Contents.json deleted file mode 100644 index c7daa37..0000000 --- a/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "gift_icon@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "gift_icon@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/gift_icon@2x.png b/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/gift_icon@2x.png deleted file mode 100644 index 9fd1f07..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/gift_icon@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/gift_icon@3x.png b/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/gift_icon@3x.png deleted file mode 100644 index 6ac7e1a..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/gift_icon.imageset/gift_icon@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/@图标/提示/红色@2x.png b/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/@图标/提示/红色@2x.png deleted file mode 100755 index faccc99..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/@图标/提示/红色@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/@图标/提示/红色@3x.png b/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/@图标/提示/红色@3x.png deleted file mode 100755 index a910ce5..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/@图标/提示/红色@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/Contents.json deleted file mode 100755 index 3c4be5f..0000000 --- a/crush/Crush/Assets.xcassets/Chat/icon_im_notice.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/@图标/提示/红色备份@2x.png b/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/@图标/提示/红色备份@2x.png deleted file mode 100755 index 69c8c87..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/@图标/提示/红色备份@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/@图标/提示/红色备份@3x.png b/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/@图标/提示/红色备份@3x.png deleted file mode 100755 index df73205..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/@图标/提示/红色备份@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/Contents.json deleted file mode 100755 index 248d6b7..0000000 --- a/crush/Crush/Assets.xcassets/Chat/icon_im_retry.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/Contents.json deleted file mode 100644 index 564142b..0000000 --- a/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "litte_status_successful@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "litte_status_successful@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/litte_status_successful@2x.png b/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/litte_status_successful@2x.png deleted file mode 100644 index e1413e4..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/litte_status_successful@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/litte_status_successful@3x.png b/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/litte_status_successful@3x.png deleted file mode 100644 index ea53d35..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/litte_status_successful.imageset/litte_status_successful@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/Contents.json deleted file mode 100644 index 7617f48..0000000 --- a/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "temp_voice_wave@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "temp_voice_wave@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/temp_voice_wave@2x.png b/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/temp_voice_wave@2x.png deleted file mode 100644 index 24d8afe..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/temp_voice_wave@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/temp_voice_wave@3x.png b/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/temp_voice_wave@3x.png deleted file mode 100644 index d3462d5..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/temp_voice_wave.imageset/temp_voice_wave@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/Contents.json deleted file mode 100644 index 39a3ca0..0000000 --- a/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "voice_hold_overlay_bot@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "voice_hold_overlay_bot@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/voice_hold_overlay_bot@2x.png b/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/voice_hold_overlay_bot@2x.png deleted file mode 100644 index 71bcb22..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/voice_hold_overlay_bot@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/voice_hold_overlay_bot@3x.png b/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/voice_hold_overlay_bot@3x.png deleted file mode 100644 index 5555c46..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/voice_hold_overlay_bot.imageset/voice_hold_overlay_bot@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/Contents.json b/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/Contents.json deleted file mode 100644 index 13dd424..0000000 --- a/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "voice_record_decoration@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "voice_record_decoration@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/voice_record_decoration@2x.png b/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/voice_record_decoration@2x.png deleted file mode 100644 index 802eaaa..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/voice_record_decoration@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/voice_record_decoration@3x.png b/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/voice_record_decoration@3x.png deleted file mode 100644 index ac934e7..0000000 Binary files a/crush/Crush/Assets.xcassets/Chat/voice_record_decoration.imageset/voice_record_decoration@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Coin/Contents.json b/crush/Crush/Assets.xcassets/Coin/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Coin/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/Contents.json b/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/Contents.json deleted file mode 100644 index 2359c3f..0000000 --- a/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "buy_credits_title_logo@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "buy_credits_title_logo@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/buy_credits_title_logo@2x.png b/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/buy_credits_title_logo@2x.png deleted file mode 100644 index 55d0b8f..0000000 Binary files a/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/buy_credits_title_logo@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/buy_credits_title_logo@3x.png b/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/buy_credits_title_logo@3x.png deleted file mode 100644 index acc7515..0000000 Binary files a/crush/Crush/Assets.xcassets/Coin/buy_credits_title_logo.imageset/buy_credits_title_logo@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/Contents.json b/crush/Crush/Assets.xcassets/Common/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Common/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/WhiteGrid.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/WhiteGrid.imageset/Contents.json deleted file mode 100644 index b607534..0000000 --- a/crush/Crush/Assets.xcassets/Common/WhiteGrid.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "WhiteGrid.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/WhiteGrid.imageset/WhiteGrid.png b/crush/Crush/Assets.xcassets/Common/WhiteGrid.imageset/WhiteGrid.png deleted file mode 100644 index 53f8b02..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/WhiteGrid.imageset/WhiteGrid.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/Contents.json deleted file mode 100644 index 9e748e9..0000000 --- a/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "status-error@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "status-error@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/status-error@2x.png b/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/status-error@2x.png deleted file mode 100644 index c4b709c..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/status-error@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/status-error@3x.png b/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/status-error@3x.png deleted file mode 100644 index ea54d92..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/album_reupload.imageset/status-error@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/Contents.json deleted file mode 100644 index 20f8b1f..0000000 --- a/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "checkmark_tick@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "checkmark_tick@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/checkmark_tick@2x.png b/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/checkmark_tick@2x.png deleted file mode 100644 index 9bf27ff..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/checkmark_tick@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/checkmark_tick@3x.png b/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/checkmark_tick@3x.png deleted file mode 100644 index 59a43d5..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/checkmark_tick.imageset/checkmark_tick@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/Contents.json deleted file mode 100644 index 80166bf..0000000 --- a/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "checkmark_tick_light@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "checkmark_tick_light@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/checkmark_tick_light@2x.png b/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/checkmark_tick_light@2x.png deleted file mode 100644 index 6577e4e..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/checkmark_tick_light@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/checkmark_tick_light@3x.png b/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/checkmark_tick_light@3x.png deleted file mode 100644 index baacd37..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/checkmark_tick_light.imageset/checkmark_tick_light@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/Contents.json deleted file mode 100644 index 36dcf8c..0000000 --- a/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "checkmark_tick_none@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "checkmark_tick_none@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/checkmark_tick_none@2x.png b/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/checkmark_tick_none@2x.png deleted file mode 100644 index 780aae8..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/checkmark_tick_none@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/checkmark_tick_none@3x.png b/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/checkmark_tick_none@3x.png deleted file mode 100644 index b39313b..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/checkmark_tick_none.imageset/checkmark_tick_none@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/Contents.json deleted file mode 100644 index a3e87d3..0000000 --- a/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "cl_logo@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "cl_logo@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/cl_logo@2x.png b/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/cl_logo@2x.png deleted file mode 100644 index 408b580..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/cl_logo@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/cl_logo@3x.png b/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/cl_logo@3x.png deleted file mode 100644 index c6b5dc0..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/cl_logo.imageset/cl_logo@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/eg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/eg.imageset/Contents.json deleted file mode 100644 index 8168777..0000000 --- a/crush/Crush/Assets.xcassets/Common/eg.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "eg.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/eg.imageset/eg.png b/crush/Crush/Assets.xcassets/Common/eg.imageset/eg.png deleted file mode 100644 index fd71afd..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/eg.imageset/eg.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/egpic.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/egpic.imageset/Contents.json deleted file mode 100644 index c36472c..0000000 --- a/crush/Crush/Assets.xcassets/Common/egpic.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "egpic.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/egpic.imageset/egpic.png b/crush/Crush/Assets.xcassets/Common/egpic.imageset/egpic.png deleted file mode 100644 index 15f9e50..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/egpic.imageset/egpic.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/Contents.json deleted file mode 100644 index 76bfa49..0000000 --- a/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "empty_placeholder_icon@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "empty_placeholder_icon@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/empty_placeholder_icon@2x.png b/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/empty_placeholder_icon@2x.png deleted file mode 100644 index b2f3490..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/empty_placeholder_icon@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/empty_placeholder_icon@3x.png b/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/empty_placeholder_icon@3x.png deleted file mode 100644 index 4de06ab..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/empty_placeholder_icon.imageset/empty_placeholder_icon@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/Contents.json deleted file mode 100644 index a1f7606..0000000 --- a/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "camera@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "camera@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/camera@2x.png b/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/camera@2x.png deleted file mode 100644 index e6a7b83..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/camera@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/camera@3x.png b/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/camera@3x.png deleted file mode 100644 index f1e696d..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_album_camera.imageset/camera@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/@图标/箭头/收起@2x.png b/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/@图标/箭头/收起@2x.png deleted file mode 100755 index 2b314a7..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/@图标/箭头/收起@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/@图标/箭头/收起@3x.png b/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/@图标/箭头/收起@3x.png deleted file mode 100755 index fab438f..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/@图标/箭头/收起@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/Contents.json deleted file mode 100755 index 9f0082b..0000000 --- a/crush/Crush/Assets.xcassets/Common/icon_close_20.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/Contents.json deleted file mode 100755 index 3e37459..0000000 --- a/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_close_20_black@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_close_20_black@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/icon_close_20_black@2x.png b/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/icon_close_20_black@2x.png deleted file mode 100755 index 4ef9427..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/icon_close_20_black@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/icon_close_20_black@3x.png b/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/icon_close_20_black@3x.png deleted file mode 100755 index 01c95a9..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_close_20_black.imageset/icon_close_20_black@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/Contents.json deleted file mode 100644 index 79f0e98..0000000 --- a/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_tag_unSel@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_tag_unSel@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/icon_tag_unSel@2x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/icon_tag_unSel@2x.png deleted file mode 100644 index 0911ebb..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/icon_tag_unSel@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/icon_tag_unSel@3x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/icon_tag_unSel@3x.png deleted file mode 100644 index e1eb7b1..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_default.imageset/icon_tag_unSel@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/Contents.json deleted file mode 100644 index a47109a..0000000 --- a/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_tag_gray_unSel@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_tag_gray_unSel@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/icon_tag_gray_unSel@2x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/icon_tag_gray_unSel@2x.png deleted file mode 100644 index 6cca89c..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/icon_tag_gray_unSel@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/icon_tag_gray_unSel@3x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/icon_tag_gray_unSel@3x.png deleted file mode 100644 index 6db9bd6..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_gray_default.imageset/icon_tag_gray_unSel@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/Contents.json deleted file mode 100644 index 2cea230..0000000 --- a/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_tag_sel@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_tag_sel@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/icon_tag_sel@2x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/icon_tag_sel@2x.png deleted file mode 100644 index 9dcfd27..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/icon_tag_sel@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/icon_tag_sel@3x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/icon_tag_sel@3x.png deleted file mode 100644 index bcf73bc..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_selected.imageset/icon_tag_sel@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/Contents.json deleted file mode 100644 index 375a1d5..0000000 --- a/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_tag_selected_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_tag_selected_bg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/icon_tag_selected_bg@2x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/icon_tag_selected_bg@2x.png deleted file mode 100644 index 7ff831f..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/icon_tag_selected_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/icon_tag_selected_bg@3x.png b/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/icon_tag_selected_bg@3x.png deleted file mode 100644 index 0bd40fa..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/icon_tag_selected_bg.imageset/icon_tag_selected_bg@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/Contents.json deleted file mode 100644 index b3dd016..0000000 --- a/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "logo_ai_role@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "logo_ai_role@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/logo_ai_role@2x.png b/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/logo_ai_role@2x.png deleted file mode 100644 index 97a1093..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/logo_ai_role@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/logo_ai_role@3x.png b/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/logo_ai_role@3x.png deleted file mode 100644 index 6fe0ebd..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/logo_ai_role.imageset/logo_ai_role@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/nav_back.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/nav_back.imageset/Contents.json deleted file mode 100644 index 7b82496..0000000 --- a/crush/Crush/Assets.xcassets/Common/nav_back.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "nav_back@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "nav_back@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/nav_back.imageset/nav_back@2x.png b/crush/Crush/Assets.xcassets/Common/nav_back.imageset/nav_back@2x.png deleted file mode 100644 index 2f07472..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/nav_back.imageset/nav_back@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/nav_back.imageset/nav_back@3x.png b/crush/Crush/Assets.xcassets/Common/nav_back.imageset/nav_back@3x.png deleted file mode 100644 index 2b2adc8..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/nav_back.imageset/nav_back@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/Contents.json deleted file mode 100644 index bb55b8f..0000000 --- a/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "nav_back_white@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "nav_back_white@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/nav_back_white@2x.png b/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/nav_back_white@2x.png deleted file mode 100644 index 9107187..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/nav_back_white@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/nav_back_white@3x.png b/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/nav_back_white@3x.png deleted file mode 100644 index 5fe7145..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/nav_back_white.imageset/nav_back_white@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/status-error.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/status-error.imageset/Contents.json deleted file mode 100644 index 9e748e9..0000000 --- a/crush/Crush/Assets.xcassets/Common/status-error.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "status-error@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "status-error@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/status-error.imageset/status-error@2x.png b/crush/Crush/Assets.xcassets/Common/status-error.imageset/status-error@2x.png deleted file mode 100644 index 7f72b65..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/status-error.imageset/status-error@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/status-error.imageset/status-error@3x.png b/crush/Crush/Assets.xcassets/Common/status-error.imageset/status-error@3x.png deleted file mode 100644 index decf19f..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/status-error.imageset/status-error@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/Contents.json b/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/Contents.json deleted file mode 100644 index c476341..0000000 --- a/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "warning_blue@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "warning_blue@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/warning_blue@2x.png b/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/warning_blue@2x.png deleted file mode 100644 index 48af49c..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/warning_blue@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/warning_blue@3x.png b/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/warning_blue@3x.png deleted file mode 100644 index ffde886..0000000 Binary files a/crush/Crush/Assets.xcassets/Common/warning_blue.imageset/warning_blue@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Contents.json b/crush/Crush/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/Contents.json b/crush/Crush/Assets.xcassets/Discover/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Discover/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/Contents.json deleted file mode 100644 index edeea16..0000000 --- a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "daily_check_bg_banner@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "daily_check_bg_banner@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/daily_check_bg_banner@2x.png b/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/daily_check_bg_banner@2x.png deleted file mode 100644 index bcebd23..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/daily_check_bg_banner@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/daily_check_bg_banner@3x.png b/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/daily_check_bg_banner@3x.png deleted file mode 100644 index 1a8d6bc..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_banner.imageset/daily_check_bg_banner@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/Contents.json deleted file mode 100644 index 445c66f..0000000 --- a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "daily_check_bg_diamond@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "daily_check_bg_diamond@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/daily_check_bg_diamond@2x.png b/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/daily_check_bg_diamond@2x.png deleted file mode 100644 index 872fca4..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/daily_check_bg_diamond@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/daily_check_bg_diamond@3x.png b/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/daily_check_bg_diamond@3x.png deleted file mode 100644 index c745aa1..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/daily_check_bg_diamond.imageset/daily_check_bg_diamond@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/Contents.json deleted file mode 100644 index 943216a..0000000 --- a/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "daily_check_line@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "daily_check_line@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/daily_check_line@2x.png b/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/daily_check_line@2x.png deleted file mode 100644 index d67c246..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/daily_check_line@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/daily_check_line@3x.png b/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/daily_check_line@3x.png deleted file mode 100644 index d482b14..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/daily_check_line.imageset/daily_check_line@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/Contents.json deleted file mode 100644 index 113dde4..0000000 --- a/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "discover_banner_ad@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "discover_banner_ad@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/discover_banner_ad@2x.png b/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/discover_banner_ad@2x.png deleted file mode 100644 index 6669c6a..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/discover_banner_ad@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/discover_banner_ad@3x.png b/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/discover_banner_ad@3x.png deleted file mode 100644 index c8ba78f..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/discover_banner_ad.imageset/discover_banner_ad@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/Contents.json deleted file mode 100644 index e90366f..0000000 --- a/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "discover_home_bg_rank@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "discover_home_bg_rank@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/discover_home_bg_rank@2x.png b/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/discover_home_bg_rank@2x.png deleted file mode 100644 index cd9c155..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/discover_home_bg_rank@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/discover_home_bg_rank@3x.png b/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/discover_home_bg_rank@3x.png deleted file mode 100644 index 9bde57b..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/discover_home_bg_rank.imageset/discover_home_bg_rank@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/Contents.json deleted file mode 100644 index dfeff42..0000000 --- a/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "discover_rank_flag_overlay@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "discover_rank_flag_overlay@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/discover_rank_flag_overlay@2x.png b/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/discover_rank_flag_overlay@2x.png deleted file mode 100644 index 20c0009..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/discover_rank_flag_overlay@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/discover_rank_flag_overlay@3x.png b/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/discover_rank_flag_overlay@3x.png deleted file mode 100644 index 4450324..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/discover_rank_flag_overlay.imageset/discover_rank_flag_overlay@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/Contents.json deleted file mode 100644 index acb4ec8..0000000 --- a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "leaderboard_flag_top1@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "leaderboard_flag_top1@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/leaderboard_flag_top1@2x.png b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/leaderboard_flag_top1@2x.png deleted file mode 100644 index 8933ba2..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/leaderboard_flag_top1@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/leaderboard_flag_top1@3x.png b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/leaderboard_flag_top1@3x.png deleted file mode 100644 index c50309b..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top1.imageset/leaderboard_flag_top1@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/Contents.json deleted file mode 100644 index af9c950..0000000 --- a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "leaderboard_flag_top2@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "leaderboard_flag_top2@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/leaderboard_flag_top2@2x.png b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/leaderboard_flag_top2@2x.png deleted file mode 100644 index f1fbba9..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/leaderboard_flag_top2@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/leaderboard_flag_top2@3x.png b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/leaderboard_flag_top2@3x.png deleted file mode 100644 index 428b821..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top2.imageset/leaderboard_flag_top2@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/Contents.json deleted file mode 100644 index 7cdf808..0000000 --- a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "leaderboard_flag_top3@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "leaderboard_flag_top3@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/leaderboard_flag_top3@2x.png b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/leaderboard_flag_top3@2x.png deleted file mode 100644 index 45c0ce2..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/leaderboard_flag_top3@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/leaderboard_flag_top3@3x.png b/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/leaderboard_flag_top3@3x.png deleted file mode 100644 index 843a6d4..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/leaderboard_flag_top3.imageset/leaderboard_flag_top3@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/Contents.json deleted file mode 100644 index d8ed7af..0000000 --- a/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "radio_checkbox_unselected@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "radio_checkbox_unselected@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/radio_checkbox_unselected@2x.png b/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/radio_checkbox_unselected@2x.png deleted file mode 100644 index e8bdfa9..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/radio_checkbox_unselected@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/radio_checkbox_unselected@3x.png b/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/radio_checkbox_unselected@3x.png deleted file mode 100644 index 77f127e..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/radio_checkbox_unselected.imageset/radio_checkbox_unselected@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/Contents.json deleted file mode 100644 index 1fa7fbf..0000000 --- a/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "role_filter_img_anime@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "role_filter_img_anime@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/role_filter_img_anime@2x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/role_filter_img_anime@2x.png deleted file mode 100644 index b8e2374..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/role_filter_img_anime@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/role_filter_img_anime@3x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/role_filter_img_anime@3x.png deleted file mode 100644 index 19a837d..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_anime.imageset/role_filter_img_anime@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/Contents.json deleted file mode 100644 index c3122f3..0000000 --- a/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "role_filter_img_filmTV@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "role_filter_img_filmTV@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/role_filter_img_filmTV@2x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/role_filter_img_filmTV@2x.png deleted file mode 100644 index 34fb202..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/role_filter_img_filmTV@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/role_filter_img_filmTV@3x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/role_filter_img_filmTV@3x.png deleted file mode 100644 index 5812317..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_filmTV.imageset/role_filter_img_filmTV@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/Contents.json deleted file mode 100644 index 5e17273..0000000 --- a/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "role_filter_img_game@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "role_filter_img_game@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/role_filter_img_game@2x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/role_filter_img_game@2x.png deleted file mode 100644 index ee1cbea..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/role_filter_img_game@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/role_filter_img_game@3x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/role_filter_img_game@3x.png deleted file mode 100644 index 792c2c7..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_game.imageset/role_filter_img_game@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/Contents.json b/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/Contents.json deleted file mode 100644 index 46056ce..0000000 --- a/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "role_filter_img_oc@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "role_filter_img_oc@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/role_filter_img_oc@2x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/role_filter_img_oc@2x.png deleted file mode 100644 index 850dacd..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/role_filter_img_oc@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/role_filter_img_oc@3x.png b/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/role_filter_img_oc@3x.png deleted file mode 100644 index e53d122..0000000 Binary files a/crush/Crush/Assets.xcassets/Discover/role_filter_img_oc.imageset/role_filter_img_oc@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/Contents.json b/crush/Crush/Assets.xcassets/Encounter/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Encounter/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/Contents.json b/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/Contents.json deleted file mode 100644 index 0259aaa..0000000 --- a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "encounter_icon_dislike@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "encounter_icon_dislike@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/encounter_icon_dislike@2x.png b/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/encounter_icon_dislike@2x.png deleted file mode 100644 index 8fca5f5..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/encounter_icon_dislike@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/encounter_icon_dislike@3x.png b/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/encounter_icon_dislike@3x.png deleted file mode 100644 index 0a1dac5..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_dislike.imageset/encounter_icon_dislike@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/Contents.json b/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/Contents.json deleted file mode 100644 index 107e0f8..0000000 --- a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "encounter_icon_like@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "encounter_icon_like@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/encounter_icon_like@2x.png b/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/encounter_icon_like@2x.png deleted file mode 100644 index 29bbf79..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/encounter_icon_like@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/encounter_icon_like@3x.png b/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/encounter_icon_like@3x.png deleted file mode 100644 index 989e444..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/encounter_icon_like.imageset/encounter_icon_like@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/Contents.json b/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/Contents.json deleted file mode 100644 index 64ecde8..0000000 --- a/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heart_meet_48@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "heart_meet_48@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/heart_meet_48@2x.png b/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/heart_meet_48@2x.png deleted file mode 100644 index 4dd15a1..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/heart_meet_48@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/heart_meet_48@3x.png b/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/heart_meet_48@3x.png deleted file mode 100644 index 67fe53a..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/heart_meet_48.imageset/heart_meet_48@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/Contents.json deleted file mode 100644 index 9776e04..0000000 --- a/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "meet_top_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "meet_top_bg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/meet_top_bg@2x.png b/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/meet_top_bg@2x.png deleted file mode 100644 index dea71b9..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/meet_top_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/meet_top_bg@3x.png b/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/meet_top_bg@3x.png deleted file mode 100644 index 8d9f5b1..0000000 Binary files a/crush/Crush/Assets.xcassets/Encounter/meet_top_bg.imageset/meet_top_bg@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/Contents.json b/crush/Crush/Assets.xcassets/Friends/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Friends/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/Contents.json deleted file mode 100644 index 30f0a9e..0000000 --- a/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "chat_level_heart@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "chat_level_heart@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/chat_level_heart@2x.png b/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/chat_level_heart@2x.png deleted file mode 100644 index 91fcf5b..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/chat_level_heart@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/chat_level_heart@3x.png b/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/chat_level_heart@3x.png deleted file mode 100644 index 12a3be7..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/chat_level_heart.imageset/chat_level_heart@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_beat_level_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/heart_beat_level_bg.imageset/Contents.json deleted file mode 100644 index 8aec8c0..0000000 --- a/crush/Crush/Assets.xcassets/Friends/heart_beat_level_bg.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heart_beat_level_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/heart_beat_level_bg.imageset/heart_beat_level_bg@2x.png b/crush/Crush/Assets.xcassets/Friends/heart_beat_level_bg.imageset/heart_beat_level_bg@2x.png deleted file mode 100644 index 78549a8..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_beat_level_bg.imageset/heart_beat_level_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/Contents.json deleted file mode 100644 index 047e1d7..0000000 --- a/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heart_little_Liked_12@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "heart_little_Liked_12@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/heart_little_Liked_12@2x.png b/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/heart_little_Liked_12@2x.png deleted file mode 100644 index 0dbc77f..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/heart_little_Liked_12@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/heart_little_Liked_12@3x.png b/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/heart_little_Liked_12@3x.png deleted file mode 100644 index c78dd55..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_little_Liked_12.imageset/heart_little_Liked_12@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/Contents.json deleted file mode 100644 index 7ce831b..0000000 --- a/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heart_wave@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "heart_wave@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/heart_wave@2x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/heart_wave@2x.png deleted file mode 100644 index fd8a7bb..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/heart_wave@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/heart_wave@3x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/heart_wave@3x.png deleted file mode 100644 index cee49df..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave.imageset/heart_wave@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/Contents.json deleted file mode 100644 index 85b51c4..0000000 --- a/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heart_wave_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "heart_wave_bg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/heart_wave_bg@2x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/heart_wave_bg@2x.png deleted file mode 100644 index af599f1..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/heart_wave_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/heart_wave_bg@3x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/heart_wave_bg@3x.png deleted file mode 100644 index 0a311ce..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave_bg.imageset/heart_wave_bg@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/Contents.json deleted file mode 100644 index a10e9e9..0000000 --- a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heart_wave_overlay_bottom@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "heart_wave_overlay_bottom@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/heart_wave_overlay_bottom@2x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/heart_wave_overlay_bottom@2x.png deleted file mode 100644 index 8a5c865..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/heart_wave_overlay_bottom@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/heart_wave_overlay_bottom@3x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/heart_wave_overlay_bottom@3x.png deleted file mode 100644 index 49ec6db..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_bottom.imageset/heart_wave_overlay_bottom@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/Contents.json deleted file mode 100644 index caff9c4..0000000 --- a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heart_wave_overlay_top@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "heart_wave_overlay_top@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/heart_wave_overlay_top@2x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/heart_wave_overlay_top@2x.png deleted file mode 100644 index 6fd7abc..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/heart_wave_overlay_top@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/heart_wave_overlay_top@3x.png b/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/heart_wave_overlay_top@3x.png deleted file mode 100644 index 7de5f8a..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heart_wave_overlay_top.imageset/heart_wave_overlay_top@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/Contents.json deleted file mode 100644 index 6d4a7d3..0000000 --- a/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "heartbeat@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "heartbeat@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/heartbeat@2x.png b/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/heartbeat@2x.png deleted file mode 100644 index ef9eb82..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/heartbeat@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/heartbeat@3x.png b/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/heartbeat@3x.png deleted file mode 100644 index 7003532..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/heartbeat.imageset/heartbeat@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/Contents.json b/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/Contents.json deleted file mode 100644 index 0b57104..0000000 --- a/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_pink_heart@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_pink_heart@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/icon_pink_heart@2x.png b/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/icon_pink_heart@2x.png deleted file mode 100644 index 8f14017..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/icon_pink_heart@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/icon_pink_heart@3x.png b/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/icon_pink_heart@3x.png deleted file mode 100644 index b8043e9..0000000 Binary files a/crush/Crush/Assets.xcassets/Friends/icon_pink_heart.imageset/icon_pink_heart@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/Contents.json b/crush/Crush/Assets.xcassets/Login/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Login/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Login/launchScreenBg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Login/launchScreenBg.imageset/Contents.json deleted file mode 100644 index 3d42aa6..0000000 --- a/crush/Crush/Assets.xcassets/Login/launchScreenBg.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "launchScreenBg.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Login/launchScreenBg.imageset/launchScreenBg.png b/crush/Crush/Assets.xcassets/Login/launchScreenBg.imageset/launchScreenBg.png deleted file mode 100644 index 643b88e..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/launchScreenBg.imageset/launchScreenBg.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/Contents.json b/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/Contents.json deleted file mode 100644 index 721f7db..0000000 --- a/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "launch_slogan@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "launch_slogan@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/launch_slogan@2x.png b/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/launch_slogan@2x.png deleted file mode 100644 index 479357d..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/launch_slogan@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/launch_slogan@3x.png b/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/launch_slogan@3x.png deleted file mode 100644 index 7a1868e..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/launch_slogan.imageset/launch_slogan@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/Contents.json deleted file mode 100644 index 19989f8..0000000 --- a/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "login_home_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "login_home_bg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/login_home_bg@2x.png b/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/login_home_bg@2x.png deleted file mode 100644 index 462de3f..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/login_home_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/login_home_bg@3x.png b/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/login_home_bg@3x.png deleted file mode 100644 index 76c1ea3..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/login_home_bg.imageset/login_home_bg@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/Contents.json b/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/Contents.json deleted file mode 100644 index 53ea0ab..0000000 --- a/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "login_home_bg_overlay@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "login_home_bg_overlay@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/login_home_bg_overlay@2x.png b/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/login_home_bg_overlay@2x.png deleted file mode 100644 index a246a19..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/login_home_bg_overlay@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/login_home_bg_overlay@3x.png b/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/login_home_bg_overlay@3x.png deleted file mode 100644 index a00e325..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/login_home_bg_overlay.imageset/login_home_bg_overlay@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/Contents.json b/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/Contents.json deleted file mode 100644 index 7f5c79f..0000000 --- a/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "login_home_bottom_overlay@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "login_home_bottom_overlay@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/login_home_bottom_overlay@2x.png b/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/login_home_bottom_overlay@2x.png deleted file mode 100644 index 39f16a7..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/login_home_bottom_overlay@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/login_home_bottom_overlay@3x.png b/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/login_home_bottom_overlay@3x.png deleted file mode 100644 index b3c05c5..0000000 Binary files a/crush/Crush/Assets.xcassets/Login/login_home_bottom_overlay.imageset/login_home_bottom_overlay@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/Contents.json b/crush/Crush/Assets.xcassets/Me/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Me/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/Contents.json deleted file mode 100644 index 6fbd8b7..0000000 --- a/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "about_us_head_logo@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "about_us_head_logo@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/about_us_head_logo@2x.png b/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/about_us_head_logo@2x.png deleted file mode 100644 index 28520d9..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/about_us_head_logo@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/about_us_head_logo@3x.png b/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/about_us_head_logo@3x.png deleted file mode 100644 index 7261eb1..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/about_us_head_logo.imageset/about_us_head_logo@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/Contents.json deleted file mode 100644 index 1766866..0000000 --- a/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "creator-diamond@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "creator-diamond@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/creator-diamond@2x.png b/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/creator-diamond@2x.png deleted file mode 100644 index c064932..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/creator-diamond@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/creator-diamond@3x.png b/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/creator-diamond@3x.png deleted file mode 100644 index 2273a3b..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator-diamond.imageset/creator-diamond@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/Contents.json deleted file mode 100644 index 4d8e7e8..0000000 --- a/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "creator-gift@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "creator-gift@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/creator-gift@2x.png b/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/creator-gift@2x.png deleted file mode 100644 index 0383e5b..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/creator-gift@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/creator-gift@3x.png b/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/creator-gift@3x.png deleted file mode 100644 index d9d9e91..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator-gift.imageset/creator-gift@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/Contents.json deleted file mode 100644 index 69514c7..0000000 --- a/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "creator-pic@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "creator-pic@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/creator-pic@2x.png b/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/creator-pic@2x.png deleted file mode 100644 index 6ccd1dc..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/creator-pic@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/creator-pic@3x.png b/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/creator-pic@3x.png deleted file mode 100644 index 6b862ab..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator-pic.imageset/creator-pic@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/Contents.json deleted file mode 100644 index 3a35343..0000000 --- a/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "creator_star_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "creator_star_bg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/creator_star_bg@2x.png b/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/creator_star_bg@2x.png deleted file mode 100644 index 94e902b..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/creator_star_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/creator_star_bg@3x.png b/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/creator_star_bg@3x.png deleted file mode 100644 index 32ede5c..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/creator_star_bg.imageset/creator_star_bg@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/Contents.json deleted file mode 100644 index 6417242..0000000 --- a/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "me_header_creator@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "me_header_creator@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/me_header_creator@2x.png b/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/me_header_creator@2x.png deleted file mode 100644 index 7c40c3e..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/me_header_creator@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/me_header_creator@3x.png b/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/me_header_creator@3x.png deleted file mode 100644 index 61d564e..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/me_header_creator.imageset/me_header_creator@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/Contents.json deleted file mode 100644 index bac68a3..0000000 --- a/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "me_header_diamonds@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "me_header_diamonds@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/me_header_diamonds@2x.png b/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/me_header_diamonds@2x.png deleted file mode 100644 index 6278d23..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/me_header_diamonds@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/me_header_diamonds@3x.png b/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/me_header_diamonds@3x.png deleted file mode 100644 index 6f5b444..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/me_header_diamonds.imageset/me_header_diamonds@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/Contents.json b/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/Contents.json deleted file mode 100644 index b94dce5..0000000 --- a/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "me_header_membership@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "me_header_membership@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/me_header_membership@2x.png b/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/me_header_membership@2x.png deleted file mode 100644 index 4e1c44a..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/me_header_membership@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/me_header_membership@3x.png b/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/me_header_membership@3x.png deleted file mode 100644 index 6eadfe0..0000000 Binary files a/crush/Crush/Assets.xcassets/Me/me_header_membership.imageset/me_header_membership@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Meet/Contents.json b/crush/Crush/Assets.xcassets/Meet/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Meet/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/Contents.json b/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/Contents.json deleted file mode 100644 index 89cca54..0000000 --- a/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "meet_big_dislike@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "meet_big_dislike@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/meet_big_dislike@2x.png b/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/meet_big_dislike@2x.png deleted file mode 100644 index 5527cb0..0000000 Binary files a/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/meet_big_dislike@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/meet_big_dislike@3x.png b/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/meet_big_dislike@3x.png deleted file mode 100644 index d76da15..0000000 Binary files a/crush/Crush/Assets.xcassets/Meet/meet_big_dislike.imageset/meet_big_dislike@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/Contents.json b/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/Contents.json deleted file mode 100644 index e012479..0000000 --- a/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "meet_big_like@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "meet_big_like@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/meet_big_like@2x.png b/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/meet_big_like@2x.png deleted file mode 100644 index 47601e4..0000000 Binary files a/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/meet_big_like@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/meet_big_like@3x.png b/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/meet_big_like@3x.png deleted file mode 100644 index 728fb95..0000000 Binary files a/crush/Crush/Assets.xcassets/Meet/meet_big_like.imageset/meet_big_like@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Role/Contents.json b/crush/Crush/Assets.xcassets/Role/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Role/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/Contents.json b/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/Contents.json deleted file mode 100644 index 758bda7..0000000 --- a/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "generating_indicator@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "generating_indicator@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/generating_indicator@2x.png b/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/generating_indicator@2x.png deleted file mode 100644 index a7ceb1c..0000000 Binary files a/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/generating_indicator@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/generating_indicator@3x.png b/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/generating_indicator@3x.png deleted file mode 100644 index 4f77464..0000000 Binary files a/crush/Crush/Assets.xcassets/Role/generating_indicator.imageset/generating_indicator@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/Contents.json b/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/Contents.json deleted file mode 100644 index 35ddccb..0000000 --- a/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_16_diamond@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_16_diamond@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/icon_16_diamond@2x.png b/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/icon_16_diamond@2x.png deleted file mode 100644 index 76ec79a..0000000 Binary files a/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/icon_16_diamond@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/icon_16_diamond@3x.png b/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/icon_16_diamond@3x.png deleted file mode 100644 index 37c20c3..0000000 Binary files a/crush/Crush/Assets.xcassets/Role/icon_16_diamond.imageset/icon_16_diamond@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/Contents.json b/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/Contents.json deleted file mode 100644 index 762bb85..0000000 --- a/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_32_diamond@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_32_diamond@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/icon_32_diamond@2x.png b/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/icon_32_diamond@2x.png deleted file mode 100644 index 5af914e..0000000 Binary files a/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/icon_32_diamond@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/icon_32_diamond@3x.png b/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/icon_32_diamond@3x.png deleted file mode 100644 index 404beef..0000000 Binary files a/crush/Crush/Assets.xcassets/Role/icon_32_diamond.imageset/icon_32_diamond@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/Contents.json deleted file mode 100644 index 26a4c3c..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_contact@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_contact@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/tabbar_contact@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/tabbar_contact@2x.png deleted file mode 100644 index 4ef5dc4..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/tabbar_contact@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/tabbar_contact@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/tabbar_contact@3x.png deleted file mode 100644 index 3837e8c..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact.imageset/tabbar_contact@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/Contents.json deleted file mode 100644 index d069cb7..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_contact_selected@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_contact_selected@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/tabbar_contact_selected@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/tabbar_contact_selected@2x.png deleted file mode 100644 index 49574e0..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/tabbar_contact_selected@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/tabbar_contact_selected@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/tabbar_contact_selected@3x.png deleted file mode 100644 index 00fd016..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_contact_selected.imageset/tabbar_contact_selected@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/Contents.json deleted file mode 100644 index 95f4941..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_create_app@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_create_app@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/tabbar_create_app@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/tabbar_create_app@2x.png deleted file mode 100644 index 40204a0..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/tabbar_create_app@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/tabbar_create_app@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/tabbar_create_app@3x.png deleted file mode 100644 index 29f7dbd..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_create_app.imageset/tabbar_create_app@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/Contents.json deleted file mode 100644 index ae1a154..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_explore@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_explore@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/tabbar_explore@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/tabbar_explore@2x.png deleted file mode 100644 index f612147..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/tabbar_explore@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/tabbar_explore@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/tabbar_explore@3x.png deleted file mode 100644 index 7a82ecb..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore.imageset/tabbar_explore@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/Contents.json deleted file mode 100644 index 9af43ef..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_explore_selected@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_explore_selected@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/tabbar_explore_selected@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/tabbar_explore_selected@2x.png deleted file mode 100644 index 5077152..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/tabbar_explore_selected@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/tabbar_explore_selected@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/tabbar_explore_selected@3x.png deleted file mode 100644 index a5d3267..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_explore_selected.imageset/tabbar_explore_selected@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/Contents.json deleted file mode 100644 index a3bf7e3..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_foryou@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_foryou@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/tabbar_foryou@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/tabbar_foryou@2x.png deleted file mode 100644 index 94bbc96..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/tabbar_foryou@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/tabbar_foryou@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/tabbar_foryou@3x.png deleted file mode 100644 index c2d1378..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou.imageset/tabbar_foryou@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/Contents.json deleted file mode 100644 index cfcf639..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_foryou_selected@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_foryou_selected@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/tabbar_foryou_selected@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/tabbar_foryou_selected@2x.png deleted file mode 100644 index 2c2592f..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/tabbar_foryou_selected@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/tabbar_foryou_selected@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/tabbar_foryou_selected@3x.png deleted file mode 100644 index 643ffb1..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_foryou_selected.imageset/tabbar_foryou_selected@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/Contents.json deleted file mode 100644 index 5bea0b4..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_me@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_me@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/tabbar_me@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/tabbar_me@2x.png deleted file mode 100644 index 9028aca..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/tabbar_me@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/tabbar_me@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/tabbar_me@3x.png deleted file mode 100644 index 0392de5..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me.imageset/tabbar_me@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/Contents.json b/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/Contents.json deleted file mode 100644 index b27cc22..0000000 --- a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tabbar_me_selected@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tabbar_me_selected@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/tabbar_me_selected@2x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/tabbar_me_selected@2x.png deleted file mode 100644 index a8366b0..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/tabbar_me_selected@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/tabbar_me_selected@3x.png b/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/tabbar_me_selected@3x.png deleted file mode 100644 index 3f91419..0000000 Binary files a/crush/Crush/Assets.xcassets/Tabbar/tabbar_me_selected.imageset/tabbar_me_selected@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/User/Contents.json b/crush/Crush/Assets.xcassets/User/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/User/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/@图标/性别/女@2x.png b/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/@图标/性别/女@2x.png deleted file mode 100644 index 1f69807..0000000 Binary files a/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/@图标/性别/女@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/@图标/性别/女@3x.png b/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/@图标/性别/女@3x.png deleted file mode 100644 index ec62efa..0000000 Binary files a/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/@图标/性别/女@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/Contents.json b/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/Contents.json deleted file mode 100644 index f529051..0000000 --- a/crush/Crush/Assets.xcassets/User/sex_female_flag.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/@图标/性别/男@2x.png b/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/@图标/性别/男@2x.png deleted file mode 100644 index ac6c917..0000000 Binary files a/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/@图标/性别/男@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/@图标/性别/男@3x.png b/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/@图标/性别/男@3x.png deleted file mode 100644 index 5944ba6..0000000 Binary files a/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/@图标/性别/男@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/Contents.json b/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/Contents.json deleted file mode 100644 index 2627fc3..0000000 --- a/crush/Crush/Assets.xcassets/User/sex_male_flag.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/@图标/性别/其他@2x.png b/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/@图标/性别/其他@2x.png deleted file mode 100644 index 9e02c14..0000000 Binary files a/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/@图标/性别/其他@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/@图标/性别/其他@3x.png b/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/@图标/性别/其他@3x.png deleted file mode 100644 index 0181df7..0000000 Binary files a/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/@图标/性别/其他@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/Contents.json b/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/Contents.json deleted file mode 100644 index 0f6155a..0000000 --- a/crush/Crush/Assets.xcassets/User/sex_no_gender_flag.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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/crush/Crush/Assets.xcassets/VIP/Contents.json b/crush/Crush/Assets.xcassets/VIP/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/VIP/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/Contents.json b/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/Contents.json deleted file mode 100644 index 8f57567..0000000 --- a/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "icon_vip_crown@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "icon_vip_crown@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/icon_vip_crown@2x.png b/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/icon_vip_crown@2x.png deleted file mode 100644 index 1525523..0000000 Binary files a/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/icon_vip_crown@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/icon_vip_crown@3x.png b/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/icon_vip_crown@3x.png deleted file mode 100644 index 911355c..0000000 Binary files a/crush/Crush/Assets.xcassets/VIP/icon_vip_crown.imageset/icon_vip_crown@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/Contents.json b/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/Contents.json deleted file mode 100644 index bc67430..0000000 --- a/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "vip_flag_16@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "vip_flag_16@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/vip_flag_16@2x.png b/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/vip_flag_16@2x.png deleted file mode 100644 index bf4a0c8..0000000 Binary files a/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/vip_flag_16@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/vip_flag_16@3x.png b/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/vip_flag_16@3x.png deleted file mode 100644 index f36f76a..0000000 Binary files a/crush/Crush/Assets.xcassets/VIP/vip_flag_16.imageset/vip_flag_16@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/Contents.json b/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/Contents.json deleted file mode 100644 index 7e98a95..0000000 --- a/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "vip_sheet_bg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "vip_sheet_bg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/vip_sheet_bg@2x.png b/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/vip_sheet_bg@2x.png deleted file mode 100644 index 98b2db8..0000000 Binary files a/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/vip_sheet_bg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/vip_sheet_bg@3x.png b/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/vip_sheet_bg@3x.png deleted file mode 100644 index a4d5a5c..0000000 Binary files a/crush/Crush/Assets.xcassets/VIP/vip_sheet_bg.imageset/vip_sheet_bg@3x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Wallet/Contents.json b/crush/Crush/Assets.xcassets/Wallet/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/crush/Crush/Assets.xcassets/Wallet/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/Contents.json b/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/Contents.json deleted file mode 100644 index 4e85d23..0000000 --- a/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "walletBg@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "walletBg@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/walletBg@2x.png b/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/walletBg@2x.png deleted file mode 100644 index de6ca85..0000000 Binary files a/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/walletBg@2x.png and /dev/null differ diff --git a/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/walletBg@3x.png b/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/walletBg@3x.png deleted file mode 100644 index 05bf2ad..0000000 Binary files a/crush/Crush/Assets.xcassets/Wallet/walletBg.imageset/walletBg@3x.png and /dev/null differ diff --git a/crush/Crush/Base.lproj/LaunchScreen.storyboard b/crush/Crush/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 5c8202d..0000000 --- a/crush/Crush/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crush/Crush/Base.lproj/Main.storyboard b/crush/Crush/Base.lproj/Main.storyboard deleted file mode 100644 index 6f65a5d..0000000 --- a/crush/Crush/Base.lproj/Main.storyboard +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crush/Crush/CL-Bridging-Header.h b/crush/Crush/CL-Bridging-Header.h deleted file mode 100644 index 67b1dd8..0000000 --- a/crush/Crush/CL-Bridging-Header.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// CL-Bridging-Header.h -// Crush -// -// Created by Leon on 2025/7/14. -// - -#ifndef CL_Bridging_Header_h -#define CL_Bridging_Header_h - -#import "EGInputLimit.h" -#import -#import "Masonry.h" -#import "UIButton+EG.h" - -#import "IapModels.h" -#import "EGIAPKeyChainStore.h" - -#import "SessionUtilOC.h" -#import - - -#endif /* CL_Bridging_Header_h */ diff --git a/crush/Crush/Crush.entitlements b/crush/Crush/Crush.entitlements deleted file mode 100644 index 0c67376..0000000 --- a/crush/Crush/Crush.entitlements +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/crush/Crush/Info.plist b/crush/Crush/Info.plist deleted file mode 100644 index 42c34ff..0000000 --- a/crush/Crush/Info.plist +++ /dev/null @@ -1,61 +0,0 @@ - - - - - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - gg.epal.lab - CFBundleURLSchemes - - crushlevel - - - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAllowsArbitraryLoadsInWebContent - - NSExceptionDomains - - - UIAppFonts - - iconfont.ttf - Poppins-Bold.ttf - Poppins-Regular.ttf - Poppins-Medium.ttf - Poppins-SemiBold.ttf - Poppins-Italic.ttf - OleoScriptSwashCaps-Regular.ttf - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - UIBackgroundModes - - audio - - - diff --git a/crush/Crush/PrefixHeader.pch b/crush/Crush/PrefixHeader.pch deleted file mode 100755 index 0852ecd..0000000 --- a/crush/Crush/PrefixHeader.pch +++ /dev/null @@ -1,18 +0,0 @@ -// -// PrefixHeader.pch -// - -#ifndef PrefixHeader_pch -#define PrefixHeader_pch - -// Include any system framework and library headers here that should be included in all compilation units. -// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. -// OC相关代码需要以下: - -// swift 桥接文件,用于OC 调用 swift 方法 -#import "CrushLevel-Swift.h" -#import "CommonDefine.h" -#import -#import - -#endif /* PrefixHeader_pch */ diff --git a/crush/Crush/PrivacyInfo.xcprivacy b/crush/Crush/PrivacyInfo.xcprivacy deleted file mode 100644 index 76757b6..0000000 --- a/crush/Crush/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,39 +0,0 @@ - - - - - NSPrivacyTracking - - NSPrivacyCollectedDataTypes - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategorySystemBootTime - NSPrivacyAccessedAPITypeReasons - - 35F9.1 - - - - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp - NSPrivacyAccessedAPITypeReasons - - C617.1 - - - - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults - NSPrivacyAccessedAPITypeReasons - - CA92.1 - - - - - \ No newline at end of file diff --git a/crush/Crush/SceneDelegate.swift b/crush/Crush/SceneDelegate.swift deleted file mode 100644 index 382acf4..0000000 --- a/crush/Crush/SceneDelegate.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// SceneDelegate.swift -// Crush -// -// Created by lyu dong on 2025/7/8. -// - -import UIKit -import URLNavigator - -let navigator = Navigator() - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let windowScene = (scene as? UIWindowScene) else { return } - - NavigationMap.initialize(navigator: navigator) -// DispatchQueue.main.asyncAfter(deadline: .now() + 5) { -// navigator.open("crushlevel://aichat/443040313704449") -// } - - setupWindowRootController(scene: windowScene) - } - - func setupWindowRootController(scene: UIWindowScene) { - let window = UIWindow(windowScene: scene) - window.tag = 1024 - //window.rootViewController = TestEntrancesController() - window.rootViewController = TabBarController() - window.makeKeyAndVisible() - self.window = window - } - - 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. - } - - func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { - guard let urlContext = URLContexts.first else { return } - let url = urlContext.url - let options = urlContext.options - - if navigator.open(url) == true { - dlog("navigator open: \(url)") - } - // print("唤起 URL:", url) - //print("来源:", options.sourceApplication ?? "未知") - - } - - -} - diff --git a/crush/Crush/Src/API/AICowApi.swift b/crush/Crush/Src/API/AICowApi.swift deleted file mode 100644 index 9a85966..0000000 --- a/crush/Crush/Src/API/AICowApi.swift +++ /dev/null @@ -1,200 +0,0 @@ -// -// AICowApi.swift -// Crush -// -// Created by Leon on 2025/8/8. -// - -import Moya - -let AICowProvider = APIConfig.useMock && AICowAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.7) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum AICowAPI { - static let useMock: Bool = true - - /// 生成任务的通用方法 - /** - GEN_PROFILE_BY_NON AI一键生成人物基础信息 AI自行创作 - GEN_PROFILE_BY_CONTENT AI一键生成人物基础信息 AI根据用户输入进行创作 - GEN_DIALOG_STYLE_BY_NON AI一键生成对话风格 AI自行创作 - GEN_DIALOG_STYLE_BY_CONTENT AI一键生成对话风格 AI根据用户输入进行创作 - GEN_PROLOGUE_BY_NON AI一键生成开场白 AI自行创作 - GEN_PROLOGUE_BY_CONTENT AI一键生成开场白 AI根据用户输入进行创作 - GEN_INTRODUCTION AI一键生成人物简介 AI总结 - IMAGE_REFERENCE_V2 图生文-参考图生成prompt - TEXT_TO_IMAGE_PROMPT_V2 文生图-生成6组不同的prompt - IMAGE_REFERENCE 图生文-参考图生成prompt - TXT_TO_IMAGE_PROMPT 文生图-生成6组不同的prompt - */ - case aiContentGenerate(params: [String: Any]) - - // MARK: AI Pics - case imageGenerateCreateTask(params: [String: Any]) - case imageGeneratedQuery(batchNo: String) - - // MARK: AI Audio - case voiceAICallRtcToken(roomId: String) - /// 语音通话操作 - case voiceCallOperate(params: [String: Any]) - /// 语音转文本 - case voiceAsr(voiceBase64: String) - /// 语音转文本v2, url:s3 mp3地址 - case voiceAsr2(url: String, aiId: Int) - /// 生成语音 - case voiceTts(params:[String: Any]) - - // MARK: Chat text suggestions - case supChatContent(aiId : Int) - /// 删除会话、删除所有消息需要调用此接口 - case aiMessageDel(params:[String: Any]) - -} - -extension AICowAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.cow)! - } - - var path: String { - switch self { - case .aiContentGenerate: - return "/web/gen/user-content-v1" - case .imageGenerateCreateTask: - return "/web/gen/image-ct" - case .imageGeneratedQuery: - return "/web/gen/image-pl" - case .voiceAICallRtcToken: - return "/web/voice-chat/gen-rtc-tk" - case .voiceCallOperate: - return "/web/voice-chat/opt" - case .voiceAsr: - return "/web/voice/asr" - case .voiceAsr2: - return "/web/voice/asr-v2" - case .voiceTts: - return "/web/voice/tts-v2" - case .supChatContent : - return "/web/gen/sup-content-v2" - case .aiMessageDel: - return "/web/ai-message/del" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case let .aiContentGenerate(params): - mParams = params - case let .imageGenerateCreateTask(params): - mParams = params - case let .imageGeneratedQuery(batchNo): - mParams.updateValue(batchNo, forKey: "batchNo") - case let .voiceAICallRtcToken(roomId): - mParams.updateValue(roomId, forKey: "roomId") - case let .voiceCallOperate(params): - mParams = params - case .voiceAsr(let voiceBase64): - mParams.updateValue(voiceBase64, forKey: "data") - case .voiceAsr2(let url, let aiId): - mParams.updateValue(url, forKey: "url") - mParams.updateValue(aiId, forKey: "aiId") - case .voiceTts(let params): - mParams = params - case .supChatContent(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .aiMessageDel(let params): - mParams = params - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .aiContentGenerate: - let content = """ - { - "content":在某一个平凡的清晨阳光透过薄薄的窗帘洒落在房间里空气中漂浮着淡淡的咖啡香气时钟滴答作响仿佛在提醒着新的一天已经开始世界在悄然运转而人们也在各自的节奏里迎接生活有人匆忙穿过街道去赶上地铁有人推开门迎接清风与日光有人依旧在梦中与自己短暂的安宁相伴而这一切都构成了城市每日的序曲生活的美好并不总是轰轰烈烈它常常隐藏在最琐碎的细节里比如早晨一杯热牛奶的温度比如路口遇见陌生人礼貌的点头比如远方的朋友忽然发来的一句问候让你觉得再孤单的日子也有温暖的痕迹我们习惯把目光投向远方幻想着某一天能抵达一个理想的未来却常常忘了眼前的每一分每一秒才是真正的存在当我们追逐着尚未抵达的目标心里常常会生出焦虑觉得自己不够快不够好不够幸运但如果静下心来仔细看看你会发现其实生命从来没有亏待过任何人它总会在不同的阶段以不同的方式让你体验成长与收获也许是一段艰难的经历让你学会坚强也许是一场意外的遇见让你感到世界柔软也许是一份迟来的鼓励让你重新点燃希望人生从不是一条直线而更像是一条蜿蜒的河流它绕过高山经过峡谷有时奔腾有时平缓而我们要做的不是预测下一个转弯会带来什么而是学会在当下的河水里尽情漂流感受每一段风景带来的独特意义很多时候我们被告知要努力要成功要拥有别人眼中的光鲜亮丽于是拼命向前不敢停歇生怕被落下生怕被否定可真正值得珍惜的却往往是那些慢下来的时刻是与家人坐在一起谈笑风生的晚餐是一个人在书桌前静静读完一本书的午后是走在公园小径上不经意抬头看见的那片湛蓝天空这些微小而真实的片段才是支撑我们走下去的力量幸福不是宏大的目标而是一点一滴的积累正如滴水汇聚成河尘埃凝聚成星无数平凡的瞬间堆叠出生命的厚度很多人说他们害怕孤独害怕失败害怕努力没有回报可是如果换一个角度想想孤独是一种与自己对话的机会失败是一次重塑的契机努力没有回报的过程里也藏着你独有的成长轨迹当我们真正理解这些或许就会少一些抱怨多一些笃定少一些焦虑多一些耐心生命的意义从来不是一个标准答案它更像是一场没有剧本的旅行你无法预知下一个站点会遇见什么也无法控制沿途的天气是否晴朗你能做的只是带着一颗开放的心勇敢走下去接受遇见的一切并在其中找到属于自己的答案而当你慢慢这样生活时你会惊讶地发现世界似乎也在悄然回应你给你更多的惊喜和美好我们每个人都在与时间赛跑却也都在与自己和解从青春的轻狂到中年的稳重再到老年的淡然每一个阶段都有它独特的价值与美丽没有什么是完全的遗憾也没有什么是彻底的圆满人生的意义就在于这种不完美中的坚持与探索当我们终于有一天回头去看那些走过的路会发现原来所谓的结果从来没有那么重要真正让人感动的始终是那些一路陪伴的风景和一路成长的自己所以请不要过于焦虑未来也不要过度执着过去珍惜当下的每一个笑容每一次拥抱每一份努力因为它们才是你生命中最真实的财富人生就像是一首没有句号的诗长短不一起承转合我们无法决定开头和结尾却能决定如何书写中间的篇章而这篇章也终将汇成属于你自己的故事无论是精彩还是平凡都是独一无二的存在当你明白这一点你就会更加从容更加勇敢地走向前方" - } - """ - return .okContent(string: content) - // Rt17nP54sd72Hz65Jy34 - // dB17Hk54Ww78Of60pj48 - // Zr17Tf56yN38Uj92TX81 2025-8-28 机器人 - // Em17xx58Cj76KM15ue60 9-25 Lumi - // Ct17xT58EJ88AF33Dt73 9-26 我自己 - // ok17in58XN88Oe39Io01 动漫风格 - // 🔥图名批次 - case .imageGenerateCreateTask: - let content = """ - { - "batchNo": "ok17in58XN88Oe39Io01" - } - """ - return .okContent(string: content) - - // PENDING、COMPLETED -// case .imageGeneratedQuery: -// let content = """ -// [ -// { -// "status": "COMPLETED", -// "imageUrl": "https://hhb.crushlevel.ai/dev/role/17563893218387148.jpg" -// }, -// { -// "status": "FAILED", -// "imageUrl": "https://hhb.crushlevel.ai/dev/role/17563893314246930.jpg" -// }, -// { -// "status": "COMPLETED", -// "imageUrl": "https://hhb.crushlevel.ai/dev/role/1756389326156188.jpg" -// }, -// { -// "status": "COMPLETED", -// "imageUrl": "https://hhb.crushlevel.ai/dev/role/17563893094825005.jpg" -// }, -// { -// "status": "COMPLETED", -// "imageUrl": "https://hhb.crushlevel.ai/dev/role/175638931389095.jpg" -// }, -// { -// "status": "COMPLETED", -// "imageUrl": "https://hhb.crushlevel.ai/dev/role/17563893179815838.jpg" -// } -// ] -// """ -// return .okContent(string: content) -// case .voiceAICallRtcToken: -// let content = """ -// { -// "token": "QEWRFQF1234fase" -// } -// """ -// return .okContent(string: content) - default: - return Data() - } - - } -} diff --git a/crush/Crush/Src/API/AIRoleApi.swift b/crush/Crush/Src/API/AIRoleApi.swift deleted file mode 100644 index 4d3733c..0000000 --- a/crush/Crush/Src/API/AIRoleApi.swift +++ /dev/null @@ -1,420 +0,0 @@ -// -// AIRoleApi.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import Moya - -let AIRoleProvider = APIConfig.useMock && AIRoleAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.5) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum AIRoleAPI { - static let useMock: Bool = true - - /// 创建或编辑AI - case aiUserCreateEdit(params: [String: Any]) - /// 我的AI角色删除 - case aiDel(id:Int) - /// 我的AI信息查询, 🔥编辑时使用 - case aiInfoMineGet(id:Int) - - /// 单独的修改AI头像接口 - case modifyAIAvatar(aiId: Int, userHead: String) - - - // MARK: AI Role query - /// 获取AI基础信息(基础查询) - case queryAIBaseInfo(aiId: Int) - /// 获取自己的AI列表 - case aiRoleListOfMine - /// 目标用户的AI列表 - case aiRoleListOfOther(userId: Int) - - case aiUserLikeOrCancel(aiId: Int, likedStatus: LikeOrCancelStatus) - - // MARK: Other Info - /// 获取AI信息统计信息 - case aiRoleStatistics(aiId: Int) - /// 获取用户获得的礼物列表 - case aiRoleGotGiftList(params: [String: Any]) - - // MARK: Album - case aiRoleAlbumList(params: [String: Any]) - case aiRoleAlbumDel(userId:Int?, albumId:Int?) - case aiRolePhotoLikeOrNo(albumId:Int?, likedStatus: LikeOrCancelStatus?) - case aiRolePhotoDefaultSet(aiUid:Int?, albumId:Int?) - case aiRoleBatchAddAlbum(params: [String: Any]) - case aiRolePhotoUnlockPriceSet(aiUid:Int?, albumId:Int?, unlockPrice:Int = 0) - - // MARK: IM User info - case imUserBaseInfo(aiId:Int) - - /// 加密图片解锁后访问 - case imUnlockAlbumImgToBrowse(params: [String: Any]) - /// 解锁加密图片 - case imUnlockAlbumImg(params: [String: Any]) - - // MARK: Relation level - case heartBeatRelationSwitch(aiId: Int, isShow: Bool) - /// 24小时未聊天扣除心动值,登陆后调用一次 - case heartBeatSubtractLanuchActiveOnce - /// 心动等级获取 - case heartBeatLevelGet(aiId: Int) - /// 购买心动值数量 - case heartBeatPurchase(aiId: Int, heartBeatVal: CGFloat) - - case heartBeatRankGet - - // MARK: Chat background - case batchAddChatBackground(params: [String: Any]) - case getChatBackgroundList(aiId: Int) - case setDefaultChatBackground(params: [String: Any]) - case deleteChatBackground(backgroundId: Int) - - // MARK: Chat setup - case chatSetupGet(aiId: Int) - case chatSetAutoPlayVoice(aiId: Int, isAutoPlayVoice: Bool) - case chatSetup(params: [String: Any]) - case chatModelSetup(params: [String: Any]) - case chatBubbleSetup(params: [String: Any]) - -} - -extension AIRoleAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.frog)! - } - - var path: String { - switch self { - case .aiUserCreateEdit: - return "/web/ai-user/create-edit" - case .aiDel: - return "/web/ai-user/del" - case .aiInfoMineGet: - return "/web/ai-user/get-my-ai-user/info" - case .modifyAIAvatar: - return "/web/ai-user/edit-head-img" - case .queryAIBaseInfo: - return "/web/ai-user-search/base-info" - case .aiRoleListOfMine: - return "/web/ai-user-search/base-list" - case .aiRoleListOfOther: - return "/web/ai-user-search/target-list" - case .aiUserLikeOrCancel: - return "/web/ai-user/like-or-cancel" - case .aiRoleStatistics: - return "/web/ai-user/stat" - case .aiRoleGotGiftList: - return "/web/ai-user-gift/list" - case .aiRoleAlbumList: - return "/web/ai-user/album-list" - case .aiRoleAlbumDel: - return "/web/ai-user/album-del" - case .aiRolePhotoLikeOrNo: - return "/web/album/like_or_cancel" - case .aiRolePhotoDefaultSet: - return "/web/ai-user/set-default-album" - case .aiRoleBatchAddAlbum: - return "/web/ai-user/batch-add-album" - case .aiRolePhotoUnlockPriceSet: - return "/web/ai-user/set-album-unlock-price" - case .imUserBaseInfo: - return "/web/ai-user-search/im-base-info" - case .imUnlockAlbumImgToBrowse: - return "/web/ai-user/view-unlock-album-img" - case .imUnlockAlbumImg: - return "/web/ai-user/unlock-album-img" - case .heartBeatRelationSwitch: - return "/web/ai-user/heartbeat-relation-switch" - case .heartBeatSubtractLanuchActiveOnce: - return "/web/ai-user/without-chat-subtract-heartbeat-val" - case .heartBeatLevelGet: - return "/web/ai-user/heartbeat-level" - case .heartBeatPurchase: - return "/web/ai-user/buy-heartbeat-val" - case .heartBeatRankGet: - return "/web/ai-user/heartbeat-rank" - case .batchAddChatBackground: - return "/web/chat-background/batch-add" - case .getChatBackgroundList: - return "/web/chat-background/list" - case .setDefaultChatBackground: - return "/web/chat-background/set-background" - case .deleteChatBackground: - return "/web/chat-background/del" - case .chatSetupGet: - return "/web/chat-set/get-my" - case .chatSetAutoPlayVoice: - return "/web/chat-set/auto-play-voice" - case .chatSetup: - return "/web/chat-set/set" - case .chatModelSetup: - return "/web/chat-set/set-chat-model" - case .chatBubbleSetup: - return "/web/chat-set/set-chat-bubble" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - - switch self { - case let .aiUserCreateEdit(params): - mParams = params - case let .aiDel(id): - mParams.updateValue(id, forKey: "aiId") - case let .aiInfoMineGet(id): - mParams.updateValue(id, forKey: "aiId") - case let .modifyAIAvatar(aiId, userHead): - mParams.updateValue(aiId, forKey: "aiId") - mParams.updateValue(userHead, forKey: "userHead") - case .queryAIBaseInfo(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .aiRoleListOfMine: - break - case .aiRoleListOfOther(let userId): - mParams.updateValue(userId, forKey: "userId") - case .aiUserLikeOrCancel(let aiId, let likedStatus): - mParams.updateValue(aiId, forKey: "aiId") - mParams.updateValue(likedStatus.rawValue, forKey: "likedStatus") - case .aiRoleStatistics(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .aiRoleGotGiftList(let params): - mParams = params - case .aiRoleAlbumList(let params): - mParams = params - case .aiRoleAlbumDel(let userId, let albumId): - mParams.updateValue(userId ?? 0, forKey: "userId") - mParams.updateValue(albumId ?? 0, forKey: "albumId") - case .aiRolePhotoLikeOrNo(let albumId , let likedStatus): - mParams.updateValue((likedStatus ?? .liked).rawValue, forKey: "likedStatus") - mParams.updateValue(albumId ?? 0, forKey: "albumId") - case .aiRolePhotoDefaultSet(let aiUid, let albumId): - mParams.updateValue(aiUid ?? 0, forKey: "aiId") - mParams.updateValue(albumId ?? 0, forKey: "albumId") - case .aiRoleBatchAddAlbum(let params): - mParams = params - case .aiRolePhotoUnlockPriceSet(let aiUid, let albumId, let unlockPrice): - mParams.updateValue(aiUid ?? 0, forKey: "aiId") - mParams.updateValue(albumId ?? 0, forKey: "albumId") - mParams.updateValue(unlockPrice, forKey: "unlockPrice") - case .imUserBaseInfo(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .imUnlockAlbumImgToBrowse(let params): - mParams = params - case .imUnlockAlbumImg(let params): - mParams = params - case .heartBeatRelationSwitch(let aiId, let isShow): - mParams.updateValue(aiId, forKey: "aiId") - mParams.updateValue(isShow, forKey: "isShow") - case .heartBeatSubtractLanuchActiveOnce: - break - case .heartBeatLevelGet(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .heartBeatPurchase(let aiId, let heartBeatVal): - mParams.updateValue(aiId, forKey: "aiId") - mParams.updateValue(heartBeatVal, forKey: "heartBeatVal") - case .heartBeatRankGet: - break - case .batchAddChatBackground(let params): - mParams = params - case .getChatBackgroundList(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .setDefaultChatBackground(let params): - mParams = params - case .deleteChatBackground(let backgroundId): - mParams.updateValue(backgroundId, forKey: "backgroundId") - case .chatSetupGet(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .chatSetAutoPlayVoice(let aiId, let isAutoPlayVoice): - mParams.updateValue(aiId, forKey: "aiId") - mParams.updateValue(isAutoPlayVoice, forKey: "isAutoPlayVoice") - case .chatSetup(let params): - mParams = params - case .chatModelSetup(let params): - mParams = params - case .chatBubbleSetup(let params): - mParams = params - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - //🔥创建AI/ Edit AI -// case .aiUserCreateEdit: -// let content = """ -// { -// "aiId" : 437416915828737 -// } -// """ -// return .okContent(string: content ) -// case .aiDel: -// return .okData() -// case .aiInfoMineGet: -// return .aiMockInfo() -// case .imageGenerateCreateTask: -// let content = """ -// { -// "batchNo": "XXXEEE" -// } -// """ -// return .okContent(string: content) -// case .imageGeneratedQuery: -// let content = """ -// [{ -// "imageUrl": "https://fastly.picsum.photos/id/871/400/400.jpg?hmac=2DvXsmLSHy27AnVwE2ceSGeY7SmL9gg-WYgEckt8fCQ", -// "status": "COMPLETED" -// }, -// { -// "imageUrl": "https://fastly.picsum.photos/id/301/200/200.jpg?hmac=8LBy-lxo8NF1vIabeRaqqBVpr2XpkwTzOSpicYy8YSU", -// "status": "COMPLETED" -// }, -// { -// "imageUrl": "https://fastly.picsum.photos/id/544/400/400.jpg?hmac=ErjxMQaSKPgH0-3sMYxdrBN9Qy8Xz8VEVd9ZtVoJEUU", -// "status": "COMPLETED" -// }, -// { -// "imageUrl": "https://fastly.picsum.photos/id/973/400/400.jpg?hmac=wLkchQD9lw5rv7FCDVlc2n2s8IB-sGw_8VN1ASZg74g", -// "status": "COMPLETED" -// }, -// { -// "imageUrl": "https://fastly.picsum.photos/id/611/400/400.jpg?hmac=dqUFohQSDmf7SOKUoEBL6RhPD7WY9RyxRYlhgZoc4zY", -// "status": "COMPLETED" -// }, -// ] -// """ -// return .okContent(string: content) -// case .queryAIBaseInfo: -// let content = """ -// { -// "aiId": 123456789, -// "idCard": "AI-000998", -// "nickname": "虚拟小可", -// "userId" : 3140, -// "sex": 1, -// "headImg": "https://fastly.picsum.photos/id/324/400/400.jpg?hmac=FNvSnDH2LM9s_74wXGut5LXk2W3XavYhnxsSWmrJlqg", -// "birthday": "2001-05-20", -// "roleName": "治愈系AI", -// "characterName": "温柔体贴", -// "tagName": "心理陪伴", -// "introduction": "我是你永远的AI朋友,随时陪伴在你左右。", -// "permission": 1, -// "homeImageUrl": "https://fastly.picsum.photos/id/324/400/400.jpg?hmac=FNvSnDH2LM9s_74wXGut5LXk2W3XavYhnxsSWmrJlqg" -// } -// """ -// return .okContent(string: content) -// case .aiRoleListOfMine: -// let content = """ -// [{ -// "aiId": 123456789, -// "idCard": "AI-000998", -// "userId" : 3140, -// "nickname": "虚拟小可", -// "sex": 1, -// "headImg": "https://fastly.picsum.photos/id/324/400/400.jpg?hmac=FNvSnDH2LM9s_74wXGut5LXk2W3XavYhnxsSWmrJlqg", -// "birthday": "2001-05-20", -// "roleName": "治愈系AI", -// "characterName": "温柔体贴", -// "tagName": "心理陪伴", -// "introduction": "我是你永远的AI朋友,随时陪伴在你左右。", -// "permission": 1, -// "homeImageUrl": "https://fastly.picsum.photos/id/324/400/400.jpg?hmac=FNvSnDH2LM9s_74wXGut5LXk2W3XavYhnxsSWmrJlqg" -// } -// ] -// """ -// return .okContent(string: content) -// case .aiRoleStatistics: -// let content = """ -// { -// "aiId": 987654321, -// "likedNum": 1234, -// "chatNum": 5678, -// "conversationNum": 4321, -// "coinNum": 876faiRolePhotoLikeOrNo -// } -// """ -// return .okContent(string: content) -// case .aiRoleAlbumList: -// let content = """ -// [{ -// "albumId": 1, -// "imgUrl": "https://fastly.picsum.photos/id/418/400/400.jpg?hmac=bb10nb5u-sK8fxD4fyTmZO36Q4N6jRTuSj-ChqtM_3M", -// "unlockPrice": 12, -// "width": "400", -// "height": "400", -// "likedCount": 10, -// "likedStatus": "CANCELED", -// "isDefault": true, -// "lockStatus": "LOCK", -// "img1": "", -// "img2": "", -// "img3": "", -// "imgOrder": 0 -// }, -// { -// "albumId": 2, -// "imgUrl": "https://fastly.picsum.photos/id/822/400/400.jpg?hmac=poBkUx6CibkZhMJfqWvaKR8uk8GbT5FC9ShX5oMGOr4", -// "unlockPrice": 12, -// "width": "400", -// "height": "400", -// "likedCount": 10, -// "likedStatus": "LIKED", -// "isDefault": false, -// "lockStatus": "UNLOCK", -// "img1": "", -// "img2": "", -// "img3": "", -// "imgOrder": 0 -// } -// ] -// """ -// return .okPageDatas(string: content) -// case .aiRoleAlbumDel: -// return .okData() -// case .aiRolePhotoLikeOrNo: -// return .okData() -// case .aiRolePhotoDefaultSet: -// return .okData() -// case .aiRoleBatchAddAlbum: -// return .okData() -// case .heartBeatRankGet: -// let json = """ -// { -// "status" : "OK", -// "errorCode" : null, -// "content" : 0.9, -// "errorMsg" : null, -// "traceId" : "a87240e3-f7a3-4a02-89fc-be4c54d37a86" -// } -// """ -// return Data(json.utf8) - default: - return Data() - } - } -} diff --git a/crush/Crush/Src/API/AuthApi.swift b/crush/Crush/Src/API/AuthApi.swift deleted file mode 100644 index abd028f..0000000 --- a/crush/Crush/Src/API/AuthApi.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// AuthApi.swift -// Crush -// -// Created by Leon on 2025/7/28. -// - -import Foundation -import Moya - -enum RegisterType: String { - case EMAIL - case MOBILE - case THIRD -} - -enum SMSCodeType: Int { - case email = 0 - case mobile_login = 1 - case reset_password = 2 -} - -enum LoginSMSCodeFrom: String { - case WHATS_APP - case MOBILE -} - -enum SendTypeEnum: String { - case SMS - case WHATS_APP -} - -enum ThirdType: String, Codable { - case APPLE = "APPLE" - case GOOGLE = "GOOGLE" - case DISCORD = "DISCORD" -} - -// let LoginProvider = MoyaProvider(requestClosure: myRequestClosure) - -// MoyaProvider.immediatelyStub -let LoginProvider = APIConfig.useMock && LoginAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.5) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum LoginAPI { - static let useMock: Bool = false - - /// 三方账号注册登陆 - case loginByThird(params: [String: Any]) - - case registerThird(thirdToken: String, thirdType: ThirdType = .APPLE, nickname: String) - /// 完善用户基础信息 - case completeUserInfo(params: [String: Any]) -} - -extension LoginAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.frog)! - } - - var path: String { - switch self { - case .loginByThird: - return "/web/third/login" - case .registerThird: - return "/mobile/user/register/by-third" - case .completeUserInfo: - return "/web/user/complete-user-info" - } - } - - var method: Moya.Method { - return .post - } - -// var sampleData: Data { -// return "".data(using: String.Encoding.utf8)! -// } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case let .loginByThird(params): - mParams = params - case let .registerThird(thirdToken, thirdType, nickname): - mParams = ["thirdToken": thirdToken, "thirdType": thirdType.rawValue, "nickname": nickname] - case let .completeUserInfo(params): - mParams = params - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - -// if let code = HCaptchaHandler.shared.reqValidateCodeForLoginAPI { -// mParams.updateValue(code, forKey: "reqValidateCode") -// } - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - static func getGMTFlagForSignUp() -> String? { - if let abbreviation = TimeZone.current.abbreviation() { - let pure = abbreviation.replacingOccurrences(of: "GMT", with: "") - if let gmt = Int(pure) { - if gmt > 0 { - let format = String(format: "+%02d", gmt) - dlog("❇️ GMT Flag: \(format)") - return format - } else { - let format = String(format: "-%02d", gmt) - dlog("❇️ GMT Flag: \(format)") - return format - } - } - } - return nil - } - - var sampleData: Data { - switch self { - case .loginByThird: - let content = """ - { - "token": "LvRK8TGeMGbPRfQWeyuSV4o0XqXkDJDN" - } - """ - return .okContent(string: content) - case .completeUserInfo: - let mockJSON = """ - { - "status" : "OK", - "errorCode" : null, - "content" : null, - "errorMsg" : null, - "traceId" : "b46cb53f-550c-46aa-8ccd-28f968f97e0a" - } - """ - return Data(mockJSON.utf8) - default: - return Data() - } - } -} diff --git a/crush/Crush/Src/API/ChatApi.swift b/crush/Crush/Src/API/ChatApi.swift deleted file mode 100644 index 335c798..0000000 --- a/crush/Crush/Src/API/ChatApi.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ChatApi.swift -// Crush -// -// Created by Leon on 2025/8/24. -// - -import Moya - -let ChatProvider = APIConfig.useMock && ChatAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.7) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum ChatAPI { - static let useMock: Bool = true - - case aiUserGiftSend(params: [String:Any]) - /// 查询解锁查看图片 - case viewAIUnlockAlbumImg(params: [String:Any]) - case aiUnlockAlbumImg(params: [String:Any]) - case letAISendPrologue(aiId: Int) -} - -extension ChatAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.frog)! - } - - var path: String { - switch self { - case .aiUserGiftSend: - return "/web/ai-user-gift/send" - case .viewAIUnlockAlbumImg: - return "/web/ai-user/view-unlock-album-img" - case .aiUnlockAlbumImg: - return "/web/ai-user/unlock-album-img" - case .letAISendPrologue: - return "/web/chat/send-dialogue-prologue-message" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case .aiUserGiftSend(let params): - mParams = params - case .viewAIUnlockAlbumImg(let params): - mParams = params - case .aiUnlockAlbumImg(let params): - mParams = params - case .letAISendPrologue(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .aiUserGiftSend: - return Data() - default: - return Data() - } - } -} diff --git a/crush/Crush/Src/API/CommonApi.swift b/crush/Crush/Src/API/CommonApi.swift deleted file mode 100644 index a2c7aee..0000000 --- a/crush/Crush/Src/API/CommonApi.swift +++ /dev/null @@ -1,203 +0,0 @@ -// -// CommonApi.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import Moya - -let CommonProvider = APIConfig.useMock && CommonAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.5) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum CommonAPI { - static let useMock: Bool = false - - case getAIDict - case getGiftDict(params: [String: Any]) - case getChatModelDict - /// 聊天气泡字典列表 - case chatBubbleDict(aiId :Int) -} - -extension CommonAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.frog)! - } - - var path: String { - switch self { - case .getAIDict: - return "/web/get-ai-dict" - case .getGiftDict: - return "/web/gift/dict-list" - case .getChatModelDict: - return "/web/chat-model/dict-list" - case .chatBubbleDict: - return "/web/chat-set/get-chat-bubble-list" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - - switch self { - case .getAIDict: - break - case .getGiftDict(let params): - mParams = params - case .getChatModelDict: - break - case .chatBubbleDict(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .getAIDict: - let mockJSON = """ - { - "content": { - "roleDictList": [ - { - "code": "Original", - "name": "原创", - "childDictList": [ - { - "code": "Original", - "name": "原创", - "childDictList": [] - } - ] - }, - { - "code": "Fanfic", - "name": "同人", - "childDictList": [ - { - "code": "Animation", - "name": "动漫", - "childDictList": [] - }, - { - "code": "Game", - "name": "游戏", - "childDictList": [] - }, - { - "code": "TV", - "name": "影视", - "childDictList": [] - } - ] - } - ], - "characterDictList": [ - { - "code": "性格", - "name": "性格", - "childDictList": [ - { - "code": "", - "name": "", - "childDictList": [] - } - ] - } - ], - "tagDictList": [ - { - "code": "", - "name": "", - "childDictList": [ - { - "code": "", - "name": "", - "childDictList": [] - } - ] - } - ], - "imageStyleDictList": [ - { - "code": "", - "name": "", - "childDictList": [ - { - "code": "12344345", - "name": "XXXX", - "childDictList": [] - }, - { - "code": "24234234", - "name": "YYYY", - "childDictList": [] - } - ] - } - ], - "imageStylePicList": [ - { - "id": 10, - "dictCode": "1", - "url": "https://fastly.picsum.photos/id/449/200/200.jpg?hmac=FD7uqDWwU1CeTIaCQGi9nY0XGVRtSV7cnadWYqPt0CU", - "sort": 0, - "isDelete": 0, - "createTime": "" - }, - { - "id": 20, - "dictCode": "2", - "url": "https://fastly.picsum.photos/id/241/200/200.jpg?hmac=F-mDYyK1xUiXjlLd5PvE8EQpTwGfFndV7mMaANDtl28", - "sort": 0, - "isDelete": 0, - "createTime": "" - } - ], - "timbreDictList": [ - { - "id": 0, - "type": 0, - "name": "", - "description": "", - "url": "", - "isDelete": 0, - "createTime": "" - } - ] - }, - "status": "OK", - "errorCode": "", - "errorMsg": "", - "traceId": "" - } - """ - return Data(mockJSON.utf8) - default: - return Data() - } - } -} diff --git a/crush/Crush/Src/API/DiscoverApi.swift b/crush/Crush/Src/API/DiscoverApi.swift deleted file mode 100644 index f3ce811..0000000 --- a/crush/Crush/Src/API/DiscoverApi.swift +++ /dev/null @@ -1,234 +0,0 @@ -// -// DiscoverApi.swift -// Crush -// -// Created by Leon on 2025/9/10. -// - -import Moya - -let DiscoverProvider = APIConfig.useMock && DiscoverAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.7) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum DiscoverAPI { - static let useMock: Bool = true - - case discoverInfo - case rankChatList - case rankHeartbeatList - case rankGiftList - /// 签到 - case dailyCheck - /// 7天签到列表 - case days7CheckList - /// 首页分类列表 - case homeRolesList(params: [String:Any]) - /// 首页推荐卡片列表 - case homeRecommentCardList(params: [String:Any]) - /// 上报滑动次数 - case meetLikeOrReport(params: [String:Any]) - case meetReportBind(aiId:Int) - /// 上报meet爱慕者推荐并返回模糊相册ID - case meetRcAndRetrunBlurAlbum - /// 解锁爱慕者, return img1,img2,im3 - case meetUnlockLikeYou(aiId: Int, albumId: Int) - /// 获取AI基础信息(Meet) - case meetCardDetail(aiId: Int) -} - -extension DiscoverAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.frog)! - } - - var path: String { - switch self { - case .discoverInfo: - return "/web/explore/info" - case .rankChatList: - return "/web/rank/chat" - case .rankHeartbeatList: - return "/web/rank/heartbeat" - case .rankGiftList: - return "/web/rank/gift" - case .dailyCheck: - return "/web/si/asi" - case .days7CheckList: - return "/web/si/list" - case .homeRolesList: - return "/web/home/classification-list" - case .homeRecommentCardList: - return "/web/home/rm-list" - case .meetLikeOrReport: - return "/web/meet/sd" - case .meetReportBind: - return "/web/meet/bd" - case .meetRcAndRetrunBlurAlbum: - return "/web/meet/rc" - case .meetUnlockLikeYou: - return "/web/ai/unlock-like-you" - case .meetCardDetail: - return "/web/home/meet-detail" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case .discoverInfo: - break - case .rankChatList: - break - case .rankHeartbeatList: - break - case .rankGiftList: - break - case .dailyCheck: - break - case .days7CheckList: - break - case .homeRolesList(let params): - mParams = params - case .homeRecommentCardList(let params): - mParams = params - case .meetLikeOrReport(let params): - mParams = params - case .meetReportBind(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - case .meetRcAndRetrunBlurAlbum: - break - case .meetUnlockLikeYou(let aiId, let albumId): - mParams.updateValue(aiId, forKey: "aiId") - mParams.updateValue(albumId, forKey: "albumId") - case .meetCardDetail(let aiId): - mParams.updateValue(aiId, forKey: "aiId") - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .discoverInfo: - return Data() -// case .meetLikeOrReport: -// let content = """ -// { -// "bd": false, -// "rc": true -// } -// """ -// return .okContent(string: content) -// case .meetReportBind: -// let content = """ -// { -// "sex": 0, -// "introduction": "Luna2.0,2000年1月1日出生,象征新开始。她长发乌黑,棕眸温柔,肤白高挑,优雅动人。出身温馨家庭,受父母良好教育,培养出独立自信性格。学业优秀,大学主修心理学。职业顺利,是知名心理咨询师,专业负责获好评。社交广泛,重视亲情友情,积极公益。兴趣广泛,爱阅读旅行。生活习惯健康,心态积极。梦想是成优秀咨询师,推动心理学发展,拥有幸福家庭。她感性温柔,如春日微风,用温暖善意感染他人,努力实现梦想,让人生精彩有意义。 ", -// "characterName": "感性", -// "likedCount": 10, -// "aiId": 441103465906177, -// "userId": 437259889475585, -// "birthday": 946684800000, -// "roleName": "原创", -// "tagName": "温柔", -// "homeImageUrl": "https://hhb.crushlevel.ai/dev/role/1756635291381298.jpg", -// "liked": true, -// "headImg": "https://hhb.crushlevel.ai/dev/role/1756635291381298.jpg", -// "nickname": "Luna2" -// } -// """ -// return .okContent(string: content) -// case .meetRcAndRetrunBlurAlbum: -// let content = """ -// { -// "aiId": 436963222421505, -// "albumId": 31, -// "height": "512", -// "img1": "https://sub.crushlevel.ai/files/b/test/436961863467009/f9d54026feb9417f8bdf03cb62fb39dd.jpg?x-oss-process=image/resize,w_468,h_600&Tag=1&v=1&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9zdWIuY3J1c2hsZXZlbC5haS9maWxlcy9iL3Rlc3QvNDM2OTYxODYzNDY3MDA5L2Y5ZDU0MDI2ZmViOTQxN2Y4YmRmMDNjYjYyZmIzOWRkLmpwZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSx3XzQ2OCxoXzYwMCZUYWc9MSZ2PTEiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjIxMTQyMjcyMDB9fX1dfQ__&Signature=rAIldTcRnB5Qa5F8ghRjm7Nc~xC0OFVf8jzsrBDuzeSllLLepHhBNadF9Nlcy0iCKXPFOyUDqLkodvl8wUeEnFX7LUuNeaQ2rC5UqxA3zZ4ReZunLDYBdth3oSmLOWcg4~0aauHXrVYvYOM~98SpQ8qYJgn6y8aCODXEn-mN59WTV0xPJIB4hcxR6~0pNxrSMYctewNLjBCMdMK80Sv506DV8ff2v7G8MMIUHN6~OLwihMS9pi5oDIgtIMj352~wJyJcF~TOK6gQBu8LRMOtXn8W0YQ9IOZ5pTxkNzh4P9ZJMWol4g~7ICctyGidpaN1Lw2QWj4CKn1p9yREgId8GQ__&Key-Pair-Id=K35ZQ246Y37QV8", -// "img2": "https://sub.crushlevel.ai/files/b/test/436961863467009/f9d54026feb9417f8bdf03cb62fb39dd.jpg?x-oss-process=image/resize,w_800,h_800&Tag=1&v=1&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9zdWIuY3J1c2hsZXZlbC5haS9maWxlcy9iL3Rlc3QvNDM2OTYxODYzNDY3MDA5L2Y5ZDU0MDI2ZmViOTQxN2Y4YmRmMDNjYjYyZmIzOWRkLmpwZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSx3XzgwMCxoXzgwMCZUYWc9MSZ2PTEiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjIxMTQyMjcyMDB9fX1dfQ__&Signature=ljYMLAZCwGy1Lsj6a3SUSqSNa5fB2jk-23vrpoDdOLnM12aP3ya0EYmn3APm-lyTtdsFf9~ykWlvo-5u3hyzP0U0Is8YjbAo-LSDdlbcBZVUJU8UzeHpClB~tncw4HjJp8xapMdibQQVHAEIQpG3dkm3~PvXncL3uyDm4tOCve2LuKZA6fWIEYA-B4nC072yw3Nf3I7DEKzyNCPDhZKjnR4Oij~LrRj3U9n6G3E46DQIMXw5SlrFTFxxbzzSZvwBLvWhj9HFHieGDaUsan9X247llamqV0r~nqCpjZWZt89idv1qwW9EwbTt17ZM~8t472W43TRsOae6SkU2Ovq7Sg__&Key-Pair-Id=K35ZQ246Y37QV8", -// "img3": "https://sub.crushlevel.ai/files/b/test/436961863467009/f9d54026feb9417f8bdf03cb62fb39dd.jpg?Tag=1&v=1&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9zdWIuY3J1c2hsZXZlbC5haS9maWxlcy9iL3Rlc3QvNDM2OTYxODYzNDY3MDA5L2Y5ZDU0MDI2ZmViOTQxN2Y4YmRmMDNjYjYyZmIzOWRkLmpwZz9UYWc9MSZ2PTEiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjIxMTQyMjcyMDB9fX1dfQ__&Signature=jHwBw0crygZp~bM6RzQH7lx01dR1oKsAhd0TYqRYI5xqNWZAEeotNEHt-UmtsRRLiykoKlbPXWriZ5bO7qiG9nqNWOAKsKQOpXk3xbxY8Mh6SWoAAMonSHQg10gEJh~vtsS6FwPXCw595tz567pPqKLSRNK0gcMNovY1o25F1pPQnnwbyDkFkt0hryqgS7SWo9Xg18SKPe-gsL746B4VDaV7CgGwE4skzUk3wkLfnAMLlTQL0dYGYUa0QKbLK-uZkC~vnX33DWz5y4dHpuLjlz3hJ0yXOMFMP3cUXZFSnIKgWeClNSFR-Vc69OFQBndeefITODP4GeSaL4gPEnuHvQ__&Key-Pair-Id=K35ZQ246Y37QV8", -// "imgUrl": "", -// "likedCount": 1, -// "lockStatus": "LOCK", -// "unlockPrice": 1, -// "width": "512" -// } -// """ -// return .okContent(string: content) -// case .meetCardDetail: -// let content = """ -// { -// "age": 25, -// "introduction": "由于“身份背景”部分内容为无意义字符,无法准确提取有效信息,仅基于现有可识别信息生成简介如下:\n\n玉米玉米玉米玉米,2000年1月1日出生,拥有「理性-独立」人格,是边界清晰的独行侠。其性格内核聚焦于守护个人精神领地,习惯以清醒的逻辑与独立姿态面对周遭,在人际交往中注重界限感,如同一位坚守自我疆域的独行者,用冷静的理性构建着属于自己的精神空间。", -// "sex": 2, -// "likedCount": 23, -// "aiId": 437304385077249, -// "imageUrl": "https://hhb.crushlevel.ai/dev/role/1754823734119375.jpg", -// "tag": "浪漫", -// "role": "游戏", -// "albumList": [ -// { -// "img1": "https://sub.crushlevel.ai/files/b/test/437301824126977/a44a827f31844729977754312bca8895.jpg?x-oss-process=image/resize,w_468,h_600&Tag=1&v=1&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9zdWIuY3J1c2hsZXZlbC5haS9maWxlcy9iL3Rlc3QvNDM3MzAxODI0MTI2OTc3L2E0NGE4MjdmMzE4NDQ3Mjk5Nzc3NTQzMTJiY2E4ODk1LmpwZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSx3XzQ2OCxoXzYwMCZUYWc9MSZ2PTEiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjIxMTQyMjcyMDB9fX1dfQ__&Signature=AQKNivKVX-1Ezl0m6jlctK8rGTmEoi4GmKKwNsbeywxFoXsVkN~Jb6zvsP92~MOO2FwUZqO-ua~i5BVwE81nhcNZGrVFV1M3IPfcS7qUxsNoWZtBZmHyCwlJ7iGrmfU9pAOiulhkIEY6vhuljpiXA7JF~px58H0EYOcklnlqtmJ45UU7aNFM-rJVzgZ8y~K80LecSapfBFtzRFKyaVNdERfdiw5hCFA6QhBjXyiT76yF14J-4KROLZTnIF-7wxI5b5ewMj7LVmV7E5LjvvpfMorUSym8KfuezOL~DfmCLsiee8j7Cp7I6b1wTxgzMHTFJh9iqmh6yy~Trvc8nAKGkw__&Key-Pair-Id=K35ZQ246Y37QV8", -// "likedStatus": "CANCELED", -// "imgUrl": null, -// "lockStatus": "LOCK", -// "likedCount": 1, -// "isDefault": false, -// "aiId": 437304385077249, -// "width": "512", -// "img3": "https://sub.crushlevel.ai/files/b/test/437301824126977/a44a827f31844729977754312bca8895.jpg?Tag=1&v=1&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9zdWIuY3J1c2hsZXZlbC5haS9maWxlcy9iL3Rlc3QvNDM3MzAxODI0MTI2OTc3L2E0NGE4MjdmMzE4NDQ3Mjk5Nzc3NTQzMTJiY2E4ODk1LmpwZz9UYWc9MSZ2PTEiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjIxMTQyMjcyMDB9fX1dfQ__&Signature=W23SRRUzr4Iwnh4rFXRl7O-VyhIytjwVYk8YC~5pBsKgJbi~Rhe2zxbiCCilPkk1RX0Iw~0~1S2rOMUAFEjx6frNQ93Vecq116qAGPpO39QQAPnAaCHnBF-tN9NEgHkbMIfltD-VMPchHM1df7iq0S0gztYkeF7QOcIt4ehCZwLAxguLNKAos1I6yx1MsyHyruofthf5ryRCFId7SyHdNVX7KPzJsfplyV6uwlPFc2fIhbb9LKlz74Wlt6qtnKk8ghVLKkRISLn2xViXijYfA0uAE-hiFwo9y2dd4Q~DYVqqhsV-XZyrAqRtr15hsfavs2zh5moJxEhI5eyxZuZkEQ__&Key-Pair-Id=K35ZQ246Y37QV8", -// "unlockPrice": 123, -// "height": "512", -// "albumId": 70, -// "img2": "https://sub.crushlevel.ai/files/b/test/437301824126977/a44a827f31844729977754312bca8895.jpg?x-oss-process=image/resize,w_800,h_800&Tag=1&v=1&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9zdWIuY3J1c2hsZXZlbC5haS9maWxlcy9iL3Rlc3QvNDM3MzAxODI0MTI2OTc3L2E0NGE4MjdmMzE4NDQ3Mjk5Nzc3NTQzMTJiY2E4ODk1LmpwZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSx3XzgwMCxoXzgwMCZUYWc9MSZ2PTEiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjIxMTQyMjcyMDB9fX1dfQ__&Signature=UW9ylJXvmVSdtVmRNhe3kMu9drclxAKKPuYbqg3r4novijWk08nXuWUfImcSUmmXkUOh-JFPjC5usCiaZM1-RlJoQktZbjwsN8YS7CK2RXUwrqX4nLuv-BaHUhhbUQIhFVPkzo0ZxcxEqd6aSU9E9cleJnmZA69Ojn6cZQhthdL0x-2facVsxTdMnoJ~3x2p4YvSPzpabfzSCrSYPhHC4EWM~IgRgRlpCn6bm5Z94CkBYlYtvt3oz9WglDDw7PaSFErBTdklFGuLlcoqKrUY~vSBiEMnTEWbwiNP1ZpHMDJ4-zFBrmaVA~TbTNU~sLGMXlJLxqlgG~vWR5rrQipzAQ__&Key-Pair-Id=K35ZQ246Y37QV8", -// "imgOrder": null -// }, -// { -// "img1": null, -// "likedStatus": "CANCELED", -// "imgUrl": "https://hhb.crushlevel.ai/dev/role/1754823734119375.jpg", -// "lockStatus": null, -// "likedCount": 0, -// "isDefault": true, -// "aiId": 437304385077249, -// "width": "1024", -// "img3": null, -// "unlockPrice": 0, -// "height": "1024", -// "albumId": 69, -// "img2": null, -// "imgOrder": null -// } -// ], -// "character": "感性", -// "heartbeatVal": 0, -// "headImg": "https://hhb.crushlevel.ai/dev/main/album/437301824126977/17548237573989721.jpg", -// "nickname": "Mock数据:玉米玉米玉米玉米" -// } -// """ -// return .okContent(string: content) - default: - return Data() - } - } -} diff --git a/crush/Crush/Src/API/FriendsApi.swift b/crush/Crush/Src/API/FriendsApi.swift deleted file mode 100644 index b063e7d..0000000 --- a/crush/Crush/Src/API/FriendsApi.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// FriendsApi.swift -// Crush -// -// Created by Leon on 2025/8/28. -// - -import Moya - -let FriendsProvider = APIConfig.useMock && FriendsAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.7) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum FriendsAPI { - static let useMock: Bool = true - - case heartbeatRelationList(params: [String:Any]) -} - -extension FriendsAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.frog)! - } - - var path: String { - switch self { - case .heartbeatRelationList: - return "/web/ai-user/heartbeat-relation-list" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case .heartbeatRelationList(let params): - mParams = params - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .heartbeatRelationList: - return Data() - } - } -} diff --git a/crush/Crush/Src/API/IMApi.swift b/crush/Crush/Src/API/IMApi.swift deleted file mode 100644 index aa965d9..0000000 --- a/crush/Crush/Src/API/IMApi.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// IMApi.swift -// Crush -// -// Created by Leon on 2025/8/19. -// - -import Moya - -let IMProvider = APIConfig.useMock && IMAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.7) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum IMAPI { - static let useMock: Bool = true - - /// 获取IM账号登陆信息 - case getIMAccount - case aiUserMsgFeedback(params: [String:Any]) - /// 系统消息统计 - case messageStat - case messageList(params: [String:Any]) -} - -extension IMAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.pigeon)! - } - - var path: String { - switch self { - case .getIMAccount: - return "/web/im-user/get-account" - case .aiUserMsgFeedback: - return "/web/fb/v1" - case .messageStat: - return "/web/message/stat" - case .messageList: - return "/web/message/list" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - switch self { - case .getIMAccount: - break - case .aiUserMsgFeedback(let params): - mParams = params - case .messageStat: - break - case .messageList(let params): - mParams = params - } - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { -// case .userBaseInfo: -// return .aiMockInfo() -// case .messageStat: -// let string = """ -// { -// "unRead" : 12, -// "latestContent" : "最新的消息blabla", -// "latestTime" : 1756360311664 -// } -// """ -// return .okContent(string: string) - default: - return Data() - } - } -} diff --git a/crush/Crush/Src/API/Network/APIConfig.swift b/crush/Crush/Src/API/Network/APIConfig.swift deleted file mode 100644 index 752ca08..0000000 --- a/crush/Crush/Src/API/Network/APIConfig.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// APIConfig.swift -// WoolniOriginalSwift -// -// Created by lyu dong on 2025/7/2. -// Copyright © 2025 lydsnm. All rights reserved. -// - -import Foundation -import Moya - -struct APIConfig { - #if CLPRODUCT - static let environment: AppEnvironment = .product - #elseif CLAPPSTORE - static let environment: AppEnvironment = .appStore - #else - static let environment: AppEnvironment = .test - #endif - - static let versionNum: Int = 100 - - static let apiLogEnable: Bool = true - - /// 🔥Mock数据总开关 - #if DEBUG - static let useMock: Bool = true - #else - static let useMock: Bool = false - #endif - - enum AppEnvironment { - case dev - case test - case product - case appStore - } - - private static var headers: [String: String]? { - return ["content-type": "application/json", - "accept": "application/json,text/plain"] - } - - static func apiHeaders() -> [String: String]? { - var updatHeaders = headers - - let platform = "IOS" // _\(Bundle.appVersion) - updatHeaders?.updateValue(platform, forKey: "platform") - - let tokenNow = UserCore.shared.token - if tokenNow.count > 0 { - updatHeaders?.updateValue(tokenNow, forKey: "AUTH_TK") - } - - updatHeaders?.updateValue("\(versionNum)", forKey: "versionNum") - - updatHeaders?.updateValue(UIDevice.UUID, forKey: "AUTH_DID") - /* - if let lan = Languages.preferedLans.first { - updatHeaders?.updateValue(lan.rawValue, forKey: "accept-language") - } - - if let userCountryCode = UserCore.shared.user?.countryCode, userCountryCode.isNotBlank{ - updatHeaders?.updateValue(userCountryCode, forKey: "country") - }else if let regioncode = Locale.current.regionCode, regioncode.count > 0{ - updatHeaders?.updateValue(regioncode, forKey: "country") - } - */ - - // --- did2 统一设备标识(加密)header传递 - // let uuid = UIDevice.UUID - // let token = UserCore.shared.token - // let str = uuid - // let key = (token + "AHkt5aUUtO6HZPid").md5().uppercased() - // let aes = try! AES(key: key, iv: "HBB4UO5kEmM4169Z") - // let encrypted = try? aes.encrypt(str.bytes) - // let result = encrypted?.toBase64() ?? "" - // updatHeaders?.updateValue(result, forKey: "dId2") - - return updatHeaders - } - - static var bear: String { - return "https://test-bear.crushlevel.ai" - } - - /// AI相关、登陆注册、用户管理、字典 - static var frog: String { - return "https://test-frog.crushlevel.ai" - } - - static var lion: String { - return "https://test-lion.crushlevel.ai" - } - - /// S3、 - static var shark: String { - return "https://test-shark.crushlevel.ai" - } - - /// 生成图片 - static var cow: String { - return "https://test-cow.crushlevel.ai" - } - - static var pigeon: String{ - return "https://test-pigeon.crushlevel.ai" - } -} diff --git a/crush/Crush/Src/API/Network/APIProvider.swift b/crush/Crush/Src/API/Network/APIProvider.swift deleted file mode 100644 index 69f1a94..0000000 --- a/crush/Crush/Src/API/Network/APIProvider.swift +++ /dev/null @@ -1,282 +0,0 @@ -// -// APIProvider.swift -// WoolniOriginalSwift -// -// Created by lyu dong on 2025/7/2. -// Copyright © 2025 lydsnm. All rights reserved. -// - -import Foundation -import Moya -import UIKit - -let myEndpointClosure = { (target: TargetType) -> Endpoint in - let url = target.baseURL.absoluteString + target.path - var task = target.task - - var endpoint = Endpoint( - url: url, - sampleResponseClosure: { .networkResponse(200, target.sampleData) }, - method: target.method, - task: task, - httpHeaderFields: target.headers - ) - return endpoint -} - -let myRequestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in - do { - var request = try endpoint.urlRequest() - request.timeoutInterval = 30 - - let token = UserCore.shared.token - if !token.isEmpty { - let body = request.httpBody ?? Data() - let str = String(data:body, encoding: .utf8) - if APIConfig.apiLogEnable { - // dlog("⚠️ request加密前参数:\(str ?? "")") - } - - /* - // 加密结果 - let key = (token + "AHkt5aUUtO6HZPid").md5().uppercased() - let aes = try AES(key: key, iv: "HBB4UO5kEmM4169Z") - let encrypted = try aes.encrypt(str!.bytes) - let result = encrypted.toBase64() - if APIConfig.apiLogEnable { - // dlog("⚠️ request加密结果:\(result)") - } - let dic = ["key": result] - - let httpBody = try JSONSerialization.data(withJSONObject: dic, options: .prettyPrinted) - request.httpBody = httpBody - */ - - - } - done(.success(request)) - } catch { - done(.failure(MoyaError.underlying(error, nil))) - } -} - -let myNetworkPlugin = NetworkActivityPlugin.init { changeType, _ in - switch changeType { - case .began: dlog("开始请求网络") - case .ended: dlog("结束请求网络") - } -} - -public enum ServiceErrorEnum: String, Codable { - case unknow = "-1" - case unknowEmpty = "" - case internalError = "00000000" - case common = "0000" - - case sign_usernotexist = "10050002" - case sign_userLoginclientNotExist = "10050003" - case sign_userNotAuthorizedClient = "10050004" - case tokenExpired = "10050005" - case tokenIllegal = "10050006" - case accountIsFrozen = "10010002" - - /// 用户未登陆 - case sign_usernotLoggedIn = "10050001" - - /// 用户账号已被冻结 - case otherAccountFrozen = "10010005" - /// 账号不存在 - case newAccount = "10010001" - /// 验证码不正确 - case smscodeIncorrect = "10031001" - /// 验证码过期 - case smscodeExpired = "10031002" - /// 用户不存在 - case userExist = "10010004" - - case mustNotBeBlank = "00000001" - - case imageCheckFailed = "10019999" - /// 生成图片超时 - case imageGenTimeOut = "8006" - /// 查看未解锁的AI照片 - case imageBrowseButUnlock = "10010011" - /// AI用户不存在 - case aiRoleNotExist = "10010012" - - /// Coin 余额不足 - case insufficentCoin = "INSUFFICIENT_BALANCE" - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(String.self) - self = ServiceErrorEnum(rawValue: rawValue) ?? .unknow - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .unknow: - // 编码成一个默认值,例如 "-999",或直接跳过 - try container.encode("unknown") - default: - try container.encode(self.rawValue) - } - } -} - -// 假设 ResponseData 是一个泛型结构体,用于包装 API 响应 -struct ResponseData: Codable { - /// OK or ERROR - let status: String? - let errorCode: ServiceErrorEnum? - let errorMsg: String? - let content: T? - let traceId : String? -} - -class ResponseContentPageData: Codable{ - var pn: Int? - var ps: Int? - var datas: [T]? - var tc: Int? - var sortType:String? - var sortField: String? -} - -class RequestPageData:Codable { - var pn : Int = 1 - var ps : Int = 20 -} - -public enum ResponseError: Error { - case serviceError(code: ServiceErrorEnum?, msg: String?) - case deserializeError - case networkError -} - -//extension ResponseError { -// enum Code: String, Codable { -// case tokenExpired -// case tokenIllegal -// case accountIsFrozen -// case sign_usernotexist -// case sign_usernotLoggedIn -// case sign_userLoginclientNotExist -// case sign_userNotAuthorizedClient -// case newAccount -// case hcaptchaNeed -// case common -// } -//} - -extension MoyaProvider { - @discardableResult - public func request(_ target: Target, - progress _: ProgressBlock? = .none, - modelType TTType: T.Type, - autoShowErrMsg: Bool = true, - completion: @escaping (Result) -> Void) -> Cancellable { - - if APIConfig.apiLogEnable { - // 🗣\(target.method)\n - var paramsLog = "" - if case let .requestParameters(parameters, _) = target.task { - if let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: [.prettyPrinted]), - let jsonString = String(data: jsonData, encoding: .utf8) { - paramsLog = "\n\(jsonString)" - } else { - paramsLog = "\(parameters)" - } - } else { - paramsLog = "\(target.task)" - } - dlog("headers: \(target.headers ?? ["": ""])\n「🗣️Request」path: ⭐️\(target.path)⭐️\n🗣️⭐️params:\(paramsLog)") // - } - - - return request(target, completion: { result in - - - switch result { - case let .success(response): - do { - // 使用 JSONDecoder 解析 response.data - let decoder = JSONDecoder() - let data = try decoder.decode(ResponseData.self, from: response.data) - - // 调试日志:将 JSON 转换为字符串用于日志输出 - if APIConfig.apiLogEnable { - let jsonObject = try JSONSerialization.jsonObject(with: response.data) - let jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) - let jsonString = String(data: jsonData, encoding: .utf8) ?? String(data: response.data, encoding: .utf8) ?? "" - dlog("👉⭐️\(target.path)⭐️ response:\n\(jsonString)") - } - - let status = data.status - let code = data.errorCode - let msg = data.errorMsg - - if (status ?? "") == "OK" { - let model = data.content - completion(.success(model)) - } else { - var toastMsg = autoShowErrMsg - - if code == .tokenExpired || code == .tokenIllegal || code == .accountIsFrozen || code == .sign_usernotexist || code == .sign_usernotLoggedIn || code == .sign_userLoginclientNotExist || code == .sign_userNotAuthorizedClient { - // ⚠️ 弹出登陆 - UserCore.shared.logout() - - if let Vc = UIWindow.getTopViewController(), (Vc is CLLoginMainController || Vc is PersonalInformationFillController) { - // do nothing. - } else { -// AppRouter.goBackRootController(jumpIndex: .home) { -// NotificationCenter.post(name: .presentSignInVc, object: nil, userInfo: nil) -// } - NotificationCenter.post(name: .presentSignInVc, object: nil, userInfo: nil) - } - } else if code == .newAccount { - // ⚠️ 不进行Toast的Msg类型 - toastMsg = false - } else if code == .insufficentCoin { - // ⚠️钱包余额不足 - // refresh wallet - toastMsg = false - - var handled = false - if let cow = target as? AICowAPI{ - switch cow{ - case .voiceCallOperate, .voiceAsr2, .voiceTts: - IMAIViewModel.shared.showChatModelInsufficentCoinSheet() - handled = true - default: - break - } - } - if handled == false{ - // 弹出充值Sheet - CLPurchase.shared.showIAPBuyCoinSheet() - } - } - - if toastMsg, let msgUnpack = msg, msgUnpack.count > 0 { - UIWindow.getTopDisplayWindow()?.makeToast(msgUnpack) - } - - dlog("⛔️error: code = \(code ?? .common), msg = \(data.errorMsg ?? "业务状态失败")") - completion(.failure(.serviceError(code: code, msg: msg))) - } - - } catch { - dlog("⛔️请求成功,但解析失败: \(error), Response:⛔️\(CodableHelper.jsonString(from: response.data) ?? "x")⛔️") - completion(.failure(.deserializeError)) - } - - case let .failure(error): - dlog("⛔️ \(target.path) 网络连接失败\(error)") - UIWindow.getTopDisplayWindow()?.makeToast("Internet connection failed") - completion(.failure(.networkError)) - } - }) - } -} diff --git a/crush/Crush/Src/API/Network/DataMock.swift b/crush/Crush/Src/API/Network/DataMock.swift deleted file mode 100644 index 0e7f097..0000000 --- a/crush/Crush/Src/API/Network/DataMock.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Untitled.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -extension Data { - static func okContent(string: String) -> Data { - guard - let contentData = string.data(using: .utf8), - let contentObject = try? JSONSerialization.jsonObject(with: contentData) - else { - return Data() - } - - let wrapper: [String: Any] = [ - "content": contentObject, - "status": "OK", - "errorCode": "", - "errorMsg": "", - "traceId": "", - ] - - return (try? JSONSerialization.data(withJSONObject: wrapper)) ?? Data() - } - - static func okData() -> Data { - let mockJSON = """ - { - "content": null, - "status": "OK", - "errorCode": "", - "errorMsg": "", - "traceId": "" - } - """ - return Data(mockJSON.utf8) - } - - static func okPageDatas(string: String)->Data{ - guard - let contentData = string.data(using: .utf8), - let contentObject = try? JSONSerialization.jsonObject(with: contentData) - else { - return Data() - } - - let wrapper: [String: Any] = [ - "content": [ - "pn": 0, - "ps": 0, - "datas":contentObject, - "tc": 0, - "sortType": "", - "sortField": "" - ], - "status": "OK", - "errorCode": "", - "errorMsg": "", - "traceId": "" - ] - - - return (try? JSONSerialization.data(withJSONObject: wrapper)) ?? Data() - } - - static func aiMockInfo() -> Data{ - let mockJSON = """ - { - "aiId": 101, - "nickname": "MockAI-001", - "sex": 1, - "headImg": "https://example.com/avatar.jpg", - "birthday": "1999-12-31", - "roleCode": "mentor", - "role": "Mentor", - "characterCode": "kind", - "character": "Kind", - "tagCode": "funny", - "tag": "Funny", - "introduction": "I am a helpful and friendly AI created to assist you.", - "permission": 1, - "imageUrl": "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI", - "aiUserExt": { - "profile": "This is a detailed profile of the mock AI.", - "dialogueStyle": "Friendly", - "dialoguePrologue": "Hi there! I'm ready to help.", - "dialogueTimbre": "Warm and clear", - "imageStyleCode": "realistic", - "imageStyle": "Realistic Illustration", - "imageStyleUrl": "https://fastly.picsum.photos/id/888/200/200.jpg?hmac=k4DxIkJ_O8YKi3TA5I9xxJYJzqpSvx3QmJlgZwHMojo", - "imageDesc": "An elegant futuristic AI with soft colors", - "imageReferenceUrl": "https://picsum.photos/200" - } - } - """ - return okContent(string: mockJSON) - } -} diff --git a/crush/Crush/Src/API/Network/LTNetwork.swift b/crush/Crush/Src/API/Network/LTNetwork.swift deleted file mode 100755 index 7a26d53..0000000 --- a/crush/Crush/Src/API/Network/LTNetwork.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// LTNetwork.swift -// LegendTeam -// -// Created by dong on 2022/1/16. -// - -import Alamofire -import Foundation - -enum ReachabilityStatus { - case notReachable - case unknown - case ethernetOrWiFi - case wwan -} - -class LTNetworkManage { - static let ltManage = LTNetworkManage() - private(set) var status: ReachabilityStatus = .unknown - let manager = NetworkReachabilityManager() - - // 新增:跟踪是否已经发送过网络恢复通知 - private var hasNotifiedNetworkRestored = false - - var reachable: Bool{ - return status != .notReachable - } - - func netWorkReachability(reachabilityStatus: ((ReachabilityStatus) -> Void)?) { - - manager!.startListening { status in - let beforeStatus = LTNetworkManage.ltManage.status - // wifi - if status == NetworkReachabilityManager.NetworkReachabilityStatus.reachable(.ethernetOrWiFi) { - print("☁️------.wifi") - LTNetworkManage.ltManage.status = .ethernetOrWiFi - reachabilityStatus?(.ethernetOrWiFi) - - // 检查是否从无网状态恢复到有网状态 - if beforeStatus == .notReachable && !LTNetworkManage.ltManage.hasNotifiedNetworkRestored { - LTNetworkManage.ltManage.hasNotifiedNetworkRestored = true - NotificationCenter.post(name: .networkRestored, object: nil, userInfo: nil) - } - } - // 不可用 - if status == NetworkReachabilityManager.NetworkReachabilityStatus.notReachable { - print("☁️------没网") - LTNetworkManage.ltManage.status = .notReachable - reachabilityStatus?(.notReachable) - - // 重置通知标志,以便下次网络恢复时可以再次通知 - LTNetworkManage.ltManage.hasNotifiedNetworkRestored = false - } - // 未知 - if status == NetworkReachabilityManager.NetworkReachabilityStatus.unknown { - print("☁️------未知") - LTNetworkManage.ltManage.status = .unknown - reachabilityStatus?(.unknown) - } - - // 蜂窝 - if status == NetworkReachabilityManager.NetworkReachabilityStatus.reachable(.cellular) { - print("☁️------蜂窝") - LTNetworkManage.ltManage.status = .wwan - reachabilityStatus?(.wwan) - - // 检查是否从无网状态恢复到有网状态 - if beforeStatus == .notReachable && !LTNetworkManage.ltManage.hasNotifiedNetworkRestored { - LTNetworkManage.ltManage.hasNotifiedNetworkRestored = true - NotificationCenter.post(name: .networkRestored, object: nil, userInfo: nil) - } - } - if beforeStatus != .unknown, beforeStatus != LTNetworkManage.ltManage.status{ - NotificationCenter.post(name: .networkChanged, object: nil, userInfo: nil) - } - } - } -} diff --git a/crush/Crush/Src/API/OssApi.swift b/crush/Crush/Src/API/OssApi.swift deleted file mode 100644 index 2da4810..0000000 --- a/crush/Crush/Src/API/OssApi.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// OssApi.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import Moya - -let OssProvider = APIConfig.useMock && OssAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.5) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum OssAPI { - static let useMock: Bool = false - - case getS3Token(bucketNameEnum: BucketS3Enum = .ROLE, suffix: SuffixS3Enum = .jpeg) - case nsfwCheck(fileFullPath: String, s3BucketEnum: String = BucketS3Enum.ALBUM.rawValue) -} - -extension OssAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.shark)! - } - - var path: String { - switch self { - case .getS3Token: - return "/web/file/sts-tk" - case .nsfwCheck: - return "/web/file/check" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case let .getS3Token(bucketNameEnum, suffix): - mParams = ["bizTypeEnum": bucketNameEnum.rawValue, "suffix": suffix.rawValue] - case let .nsfwCheck(fileFullPath, s3BucketEnum): - mParams = ["fileFullPath": fileFullPath, "bizTypeEnum": s3BucketEnum] - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .getS3Token: - #warning("to do") - let mockJSON = """ - { - - } - """ - return Data(mockJSON.utf8) - case .nsfwCheck: - let mockJSON = """ - { - - } - """ - return Data(mockJSON.utf8) - } - } -} diff --git a/crush/Crush/Src/API/UserApi.swift b/crush/Crush/Src/API/UserApi.swift deleted file mode 100644 index 67b6830..0000000 --- a/crush/Crush/Src/API/UserApi.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// UserApi.swift -// Crush -// -// Created by Leon on 2025/7/28. -// - -import Moya - -//let UserProvider = MoyaProvider(requestClosure: myRequestClosure) - -let UserProvider = APIConfig.useMock && UserAPI.useMock -? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if(data.count > 0){ - return .delayed(seconds: 0.5) - }else{ - return .never - } -}) -: MoyaProvider(requestClosure: myRequestClosure) - -enum UserAPI { - static let useMock: Bool = false - - case userInfoSelfGet - case userInfoGet(userId: NSInteger) - case userSignout - case userDel //(checkCode: String = "", messageMediaTypeEnum: RegisterType = .MOBILE) - case userInfoEdit(params: [String: Any]) - case userNicknameCheck(nickname:String, exUserId: Int?) - - // MARK: 次数 - /// 获取用户各种创作次数 - case getUserCreateCount - /// 购买创作次数 - case buyUserCreateCount(count: Int) -} - -extension UserAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.frog)! - } - - var path: String { - switch self { - case .userInfoSelfGet: - return "/web/user/base-info" - case .userInfoGet: - return "/web/user/base-info" - case .userSignout: - return "/web/user/logout" - case .userDel: - return "/web/user/del" - case .userInfoEdit: - return "/web/user/edit-user-info" - case .userNicknameCheck: - return "/web/user/nickname-check" - case .getUserCreateCount: - return "/web/user/get-user-create-count" - case .buyUserCreateCount: - return "/web/ai/buy-create-image-count" - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case .userInfoSelfGet: - return .requestPlain - case let .userInfoGet(userId): - mParams = ["userId": userId] - case .userSignout: - return .requestPlain - case .userDel: // let .userDel(checkCode, messageMediaTypeEnum): -// mParams = ["messageMediaTypeEnum": messageMediaTypeEnum.rawValue] -// if checkCode.count > 0 { -// mParams.updateValue(checkCode, forKey: "checkCode") -// } - return .requestPlain - case let .userInfoEdit(params): - mParams = params - case let .userNicknameCheck(nickname, exUserId): - mParams.updateValue(nickname, forKey: "nickname") - if let userid = exUserId{ - mParams.updateValue(userid, forKey: "exUserId") - } - case .getUserCreateCount: - break - case .buyUserCreateCount(let count): - mParams.updateValue(count, forKey: "count") - } - - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - // if let code = HCaptchaHandler.shared.reqValidateCodeForLoginAPI { - // mParams.updateValue(code, forKey: "reqValidateCode") - // } - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .userInfoSelfGet: - let content = """ - { - "idCard": "52163422534", - "headImage": "https://hhb.crushlevel.ai/dev/main/headImage/1952268491211472898/17545384837179865.jpeg", - "thirdNickname": "woolni555", - "nickname": "Rrrrrrrrrerr", - "cpUserInfo": true, - "thirdEmail": null, - "birthday": 1074006749000, - "thirdType": "DISCORD", - "userId": 1952268491211473000, - "sex": 1 - } - """ - return .okContent(string: content) - case .userInfoGet: - return .okData() - case .userSignout: - return .okData() - case .userDel: - return .okData() - case .userInfoEdit: - return .okData() - default: - return Data() - } - } -} - diff --git a/crush/Crush/Src/API/WalletApi.swift b/crush/Crush/Src/API/WalletApi.swift deleted file mode 100644 index 49bbf29..0000000 --- a/crush/Crush/Src/API/WalletApi.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// WalletApi.swift -// Crush -// -// Created by Leon on 2025/9/10. -// - -import Moya - -let WalletProvider = APIConfig.useMock && WalletAPI.useMock - ? MoyaProvider(endpointClosure: myEndpointClosure, stubClosure: { target in - let data = target.sampleData - if data.count > 0 { - return .delayed(seconds: 0.7) - } else { - return .never - } - }) - : MoyaProvider(requestClosure: myRequestClosure) - -enum WalletAPI { - static let useMock: Bool = true - /// 钱包详情 - case myWallet - /// 流水 - case billList(params: [String: Any]) - /// 充值预下单 - case tradePrecharge(params: [String:Any]) - /// Coin上传苹果票据 - case iapUploadCoinReceipt(params: [String: Any]) - /// 订阅上传苹果票据 - case iapUploadSubscribeAppleReceipt(params: [String:Any]) - /// 会员详情 - case vipDetail - - /// 订阅档位列表 - case vipTierProducts(params: [String: Any]) - /// 虚拟货币充值档位 - case coinTierProducts(params: [String: Any]) -} - -extension WalletAPI: TargetType { - var baseURL: URL { - return URL(string: APIConfig.lion)! - } - - var path: String { - switch self { - case .myWallet: - return "/web/pay/account/wallet" - case .billList: - return "/web/pay/account/bill-list" - case .tradePrecharge: - return "/web/pay/trade/pre-charge-iap" - case .iapUploadCoinReceipt: - return "/web/pay/webhooks/iap" - case .iapUploadSubscribeAppleReceipt: - return "/web/pay/subscribe/upload-apple-receipt" - case .vipDetail: - return "/web/member/detail" - case .vipTierProducts: - return "/web/pay/config/sub-product-list" - case .coinTierProducts: - return "/web/pay/config/charge-product-list" - - } - } - - var method: Moya.Method { - return .post - } - - var task: Task { - var mParams = [String: Any]() - - switch self { - case .myWallet: - break - case .billList(let params): - mParams = params - case .tradePrecharge(let params): - mParams = params - case .iapUploadCoinReceipt(let params): - mParams = params - case .iapUploadSubscribeAppleReceipt(let params): - mParams = params - case .vipDetail: - break - case .vipTierProducts(let params): - mParams = params - case .coinTierProducts(let params): - mParams = params - } - - mParams.updateValue("IOS", forKey: "appClient") - mParams.updateValue(UIDevice.UUID, forKey: "deviceCode") - - return .requestParameters(parameters: mParams, encoding: JSONEncoding.default) - } - - var headers: [String: String]? { - return APIConfig.apiHeaders() - } - - var sampleData: Data { - switch self { - case .myWallet: - return Data() - default: - return Data() - } - } -} diff --git a/crush/Crush/Src/Components/Audio/AudioPlayTool.swift b/crush/Crush/Src/Components/Audio/AudioPlayTool.swift deleted file mode 100755 index 120fae2..0000000 --- a/crush/Crush/Src/Components/Audio/AudioPlayTool.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// AudioPlayTool.swift -// LegendTeam -// -// Created by dong on 2022/1/2. -// - -import Foundation -import UIKit -import AVFAudio - -enum VoicePlayViewState { - case normal - case loading - case playing -} - -class AudioPlayTool: NSObject { - public var loadCompleteBlock: ((_ url: URL?) -> Void)? - public var playFinishBlock: ((_ url: URL?) -> Void)? - public var playProgressBlock: ((_ secconds: Int?) -> Void)? - - private(set) var audioPlayer: AVAudioPlayer? - - private(set) var playingUrl: URL? - - private(set) var state: VoicePlayViewState = .normal - - private var timer: Timer? - - private var allowPlay: Bool = false - - - override init() { - super.init() - NotificationCenter.default.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - } - - deinit { - if let isPlaying = audioPlayer?.isPlaying, isPlaying == true { - audioPlayer?.stop() - audioPlayer = nil - } - NotificationCenter.default.removeObserver(self) - print("♻️ dealloc audio player") - } - - public class func audioChannelFreeToUse() -> Bool{ -// guard PhoneManager.isInPhoneChannel() == false && ChatRoomRoute.isChatRoomOpen() == false && PhoneManager.shared().isPlayingRing == false else{ -// UIWindow.key?.makeToast("Currently using other voice communication services, please try again later") -// return false -// } - return true - } - - // 网络或者在线 - public func load(url: URL?, complete: ((_ url: URL?) -> Void)?) { - stop() - - allowPlay = true - - loadCompleteBlock = complete - guard let audioUrl = url else { - return - } - playingUrl = audioUrl - state = .loading - - DispatchQueue.global().async { [weak self] in - do { - let data = try Data(contentsOf: url!) - - if let allow = self?.allowPlay, allow == true { - self?.audioPlayer = try AVAudioPlayer(data: data) - // try audioPlayer = AVAudioPlayer(contentsOf: audioUrl) - self?.audioPlayer?.delegate = self - self?.audioPlayer!.prepareToPlay() - if self?.loadCompleteBlock != nil { - DispatchQueue.main.async { [weak self] in - self?.loadCompleteBlock!(url) - } - } - } else { // eg: 加载过程中,停止,应该直接返回 - self?.stop() - } - - } catch {} - } - } - - // 支持 base64 字符串初始化音频 - public func load(base64String: String?, complete: ((_ url: URL?) -> Void)?) { - stop() - - allowPlay = true - loadCompleteBlock = complete - - guard let base64Str = base64String, !base64Str.isEmpty else { - complete?(nil) - return - } - - state = .loading - - DispatchQueue.global().async { [weak self] in - do { - guard let data = Data(base64Encoded: base64Str) else { - DispatchQueue.main.async { [weak self] in - self?.loadCompleteBlock?(nil) - } - return - } - - if let allow = self?.allowPlay, allow == true { - self?.audioPlayer = try AVAudioPlayer(data: data) - self?.audioPlayer?.delegate = self - self?.audioPlayer!.prepareToPlay() - - if self?.loadCompleteBlock != nil { - DispatchQueue.main.async { [weak self] in - self?.loadCompleteBlock?(nil) // base64 没有 URL,传 nil - } - } - } else { // eg: 加载过程中,停止,应该直接返回 - self?.stop() - } - - } catch { - DispatchQueue.main.async { [weak self] in - self?.loadCompleteBlock?(nil) - } - } - } - } - - func isPlaying() -> Bool { - audioPlayer?.isPlaying ?? false - } - - public func play() { - let session = AVAudioSession.sharedInstance() - try? session.setCategory(AVAudioSession.Category.playAndRecord) - try? session.overrideOutputAudioPort(.speaker) - try? session.setActive(true) - - if playProgressBlock != nil { - startTimer() - } - audioPlayer?.play() - state = .playing - } - - public func stop() { - state = .normal - if let isPlaying = audioPlayer?.isPlaying, isPlaying == true { - audioPlayer?.stop() - playFinishBlock?(playingUrl) - clearTimer() - } - allowPlay = false - } - - public func setupListenPlayProgress(block: ((_ secconds: Int?) -> Void)?) { - playProgressBlock = block - } - - // MARK: - helper - - private func clearTimer() { - if timer != nil { - timer?.invalidate() - timer = nil - } - } - - private func startTimer() { - clearTimer() - let myTimer = Timer(timeInterval: 0.1, target: self, selector: #selector(timerDidChanged), userInfo: nil, repeats: true) - RunLoop.current.add(myTimer, forMode: .common) - myTimer.fire() - timer = myTimer - } - - // MARK: - action - - @objc private func timerDidChanged() { - if let audiotime = audioPlayer?.currentTime, let duration = audioPlayer?.duration { - let seconds = Int(ceil(duration - audiotime)) - - // dlog("player's current time: \(String(describing: audioPlayer?.currentTime)) seconds: \(seconds)") - playProgressBlock?(seconds) - } - } - - // MARK: - noti - - @objc private func enterBackground() { - stop() - } -} - -// MARK: - AVAudioPlayerDelegate - -extension AudioPlayTool: AVAudioPlayerDelegate { - func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - state = .normal - clearTimer() - DispatchQueue.main.async { [weak self] in - self?.playFinishBlock?(self?.playingUrl) - } - } - - func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {} - - func audioPlayerBeginInterruption(_ player: AVAudioPlayer) {} - - func audioPlayerEndInterruption(_ player: AVAudioPlayer, withOptions flags: Int) {} -} diff --git a/crush/Crush/Src/Components/Audio/AudioRecordTool.swift b/crush/Crush/Src/Components/Audio/AudioRecordTool.swift deleted file mode 100755 index 6aa6cee..0000000 --- a/crush/Crush/Src/Components/Audio/AudioRecordTool.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// AudioRecordTool.swift -// LegendTeam -// -// Created by dong on 2022/1/2. -// - -import Foundation -import AVFoundation - -@objcMembers class AudioRecordTool: NSObject { - var timeCount: Int = 0 - var timer: Timer? - var audioRecorder: AVAudioRecorder? - var audioURL: URL? - - var audioTmpName: String = "" - - public var timerChangedBlock: ((_ index: Int) -> Void)? - public var comleteBlock: ((_ path: URL?) -> Void)? - - override init() { - super.init() - audioTmpName = generateAudioName() - } - - @discardableResult - static func audioAuth(showAlert: Bool = true) -> Bool { - let authStatus = AVCaptureDevice.authorizationStatus(for: .audio) - if authStatus == .notDetermined { - let audioSession = AVAudioSession.sharedInstance() - audioSession.requestRecordPermission { _ in - // 这里allowed 为false时,看是否需要弹一个弹窗 - } - return false - } else if authStatus == .restricted || authStatus == .denied { - let alertContent = "Please open the microphone permission in the phone settings" -// if let existing = Alert.existingAlertObj() as? Alert, existing.textView.text == alertContent{ -// return false -// } - guard showAlert else{ - return false - } - let alert = Alert(title: "Notice", text: alertContent) - let cancelAction = AlertAction(title: "Cancel", actionStyle: .cancel, block: nil) - let confirmAction = AlertAction(title: "Confirm", actionStyle: .confirm) { - let url = URL(string: UIApplication.openSettingsURLString) - if let url = url, UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - alert.addAction(confirmAction) - alert.addAction(cancelAction) - alert.show() - return false - } else { - // 已授权 - return true - } - } - - public func startRecord(result: ((_ path: URL?) -> Void)?) { - timeCount = 0 - clearTimer() - - comleteBlock = result - - do { - let session = AVAudioSession.sharedInstance() - try session.setCategory(AVAudioSession.Category.playAndRecord) - try session.overrideOutputAudioPort(.speaker) - try session.setActive(true) - if #available(iOS 13.0, *) { - try session.setAllowHapticsAndSystemSoundsDuringRecording(true) - } else { - // Fallback on earlier versions - } - - let basePath: String = NSTemporaryDirectory() - let fileName = generateAudioName() - audioTmpName = fileName - let pathComponents = [basePath, fileName] - audioURL = NSURL.fileURL(withPathComponents: pathComponents)! - - // Create Audiorecorder Object - audioRecorder = try AVAudioRecorder(url: audioURL!, settings: recordSetting()) - let resultPrepare = audioRecorder!.prepareToRecord() - dlog("🔥 result:\(resultPrepare)") - - doRecord(fileName: fileName) - - } catch let error as NSError { - print("! Error coded by developer with 24g45h4qh. Default details: ", error) - } - } - - public func stopRecord() { - CLTool.feedbackGenerator() - audioRecorder?.stop() - clearTimer() - comleteBlock?(audioURL) - } - - // MARK: - helper - - private func doRecord(fileName: String) { - if audioRecorder!.isRecording { - dlog("⚠️已经在录音了") - } else { - // start recording - if let state = audioRecorder?.record(), state == true { - let myTimer = Timer(timeInterval: 0.1, target: self, selector: #selector(timerDidChanged), userInfo: nil, repeats: true) - RunLoop.current.add(myTimer, forMode: .common) - myTimer.fire() - timer = myTimer - // call immediately - timerDidChanged() - } - } - } - - private func clearTimer() { - if timer != nil { - timer?.invalidate() - timer = nil - } - } - - private func generateAudioName() -> String { - let date = Date() - return "\(date.milliStamp).m4a" - } - - private func recordSetting() -> [String: AnyObject] { - var settings: [String: AnyObject] = [:] - settings[AVFormatIDKey] = Int(kAudioFormatMPEG4AAC) as AnyObject - settings[AVSampleRateKey] = 44100.0 as AnyObject // 11025, 44100, 22050.0 - settings[AVNumberOfChannelsKey] = 2 as AnyObject - return settings - } - - // MARK: - action - - @objc private func timerDidChanged() { - timeCount += 1 - timerChangedBlock?(timeCount) - //dlog(timeCount) - } - - deinit { - clearTimer() - CLTool.clearTempFolder() - } -} diff --git a/crush/Crush/Src/Components/Audio/SpeechManager.swift b/crush/Crush/Src/Components/Audio/SpeechManager.swift deleted file mode 100644 index 83a6b59..0000000 --- a/crush/Crush/Src/Components/Audio/SpeechManager.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// SpeechManager.swift -// Crush -// -// Created by Leon on 2025/8/1. -// - -import Foundation - -class SpeechManager { - // MARK: - Properties - static let shared = SpeechManager() - private weak var currentModel: SpeechModel? - private var timer: TimerModel? - private var timerCount: Int = 0 - - private init() {} - - // MARK: - Private Methods - private func model(withPath path: String) -> SpeechModel { - let model = SpeechModel() - model.path = path - return model - } - - private func timerAction() { - timerCount -= 1 - DispatchQueue.main.async {[weak self] in - guard let `self` = self else { - return - } - currentModel?.timerChangedBlock?(TimeInterval(timerCount/10)) - } - if let model = currentModel, model.isPlaying { - if timerCount <= 0 { - stopPlay(with: model) - } - } else { - timer?.pauseTimer() - } - if timerCount < -10000 { - timerCount = 0 - } - //dlog("SpeechManager timerAction___\(timerCount)") - } - - // MARK: - Lazy Timer - private var timerInstance: TimerModel { - if timer == nil { - timer = TimerModel() - timer?.timerBlock = { [weak self] in - self?.timerAction() - } - } - return timer! - } - - // MARK: - Public Methods - static func checkVoiceState() -> Bool { - // 🔥 Assuming PhoneManager and ChatRoomRoute are external dependencies - if PhoneManager.isInPhoneChannel() || ChatRoomRoute.isChatRoomOpen() { - UIWindow.key?.makeToast("toast_voice_channel_is_occupied") - dlog("toast_voice_channel_is_occupied") - return false - } - return true - } - - func modelWithFilePath(_ path: String) -> SpeechModel { - let model = model(withPath: path) - model.fileType = .file - return model - } - - func modelWithFileUrl(_ url: String) -> SpeechModel { - let model = model(withPath: url) - model.fileType = .url - return model - } - - func modelWithBase64String(_ base64String: String) -> SpeechModel { - let model = model(withPath: base64String) - model.fileType = .base64 - return model - } - - func startPlay(with model: SpeechModel) { - guard model.loadState != .loading else { return } - - if currentModel === model { - if currentModel?.isPlaying == true { - currentModel?.stopPlay() - return - } - } - - if let current = currentModel { - stopPlay(with: current) - } - - model.canAutoPlay = true - if model.needLoadData() { - model.loadSpeechFile() - currentModel = model - } else { - currentModel = model - timerCount = model.audioTime - model.startPlay() - timerInstance.startTimer() - } - } - - func stopPlay(with model: SpeechModel) { - model.canAutoPlay = false - // model.stateChangedBlock = nil - model.stopPlay() - if model === currentModel { - timerInstance.pauseTimer() - } - } - - func stopPlayCurrent() { - if let model = currentModel { - stopPlay(with: model) - } - } -} - -// 🔥 Placeholder for external dependencies -class PhoneManager { - static func isInPhoneChannel() -> Bool { return false } // 🔥 Placeholder -} - -class ChatRoomRoute { - static func isChatRoomOpen() -> Bool { return false } // 🔥 Placeholder -} diff --git a/crush/Crush/Src/Components/Audio/SpeechModel.swift b/crush/Crush/Src/Components/Audio/SpeechModel.swift deleted file mode 100644 index e3dbbef..0000000 --- a/crush/Crush/Src/Components/Audio/SpeechModel.swift +++ /dev/null @@ -1,337 +0,0 @@ -// -// SpeechModel.swift -// Crush -// -// Created by Leon on 2025/8/1. -// - -import Foundation -import AVFoundation - -// MARK: - Enums -enum SpeechPlayState: Int { - case `default` // Default state - case playing // Playing - case complete // Playback completed - case failed // Playback failed -} - -enum SpeechLoadState: Int { - case `default` // Default state - case loading // Loading - case complete // Loading completed - case failed // Loading failed -} - -enum EGAudioFileType: Int { - case file // Local audio file - case url // Remote audio file - case base64 // Base64 encoded audio data -} - -class SpeechModel: NSObject, AVAudioPlayerDelegate { - // MARK: - Properties - var playState: SpeechPlayState = .default - var loadState: SpeechLoadState = .default - var fileType: EGAudioFileType = .file - var path: String = "" - var canAutoPlay: Bool = false - var stateChangedBlock: ((SpeechModel) -> Void)? - var timerChangedBlock: ((TimeInterval) -> Void)? - var progressChangedBlock: ((TimeInterval, TimeInterval) -> Void)? // 播放进度更新block (当前时间, 总时长) - var audioDuration: TimeInterval = 0.0 // 音频总时长(秒) - private var player: AVAudioPlayer? - private var progressTimer: Timer? - - // MARK: - Computed Properties - /// 时长 (秒x10) - var audioTime: Int { - guard let player = player else { return 0 } - let isOtherPlaying = AVAudioSession.sharedInstance().isOtherAudioPlaying - return Int(ceil(player.duration * 10)) + (isOtherPlaying ? 15 : 5) - } - - var isPlaying: Bool { - return player?.isPlaying ?? false - } - - var currentTime: TimeInterval { - get { player?.currentTime ?? 0 } - set { player?.currentTime = newValue } - } - - class func modelWith(path:String?) -> SpeechModel?{ - guard let pathValid = path else{ - return nil - } - let model = SpeechModel() - model.path = pathValid - model.fileType = .url - return model - } - - class func modelWithBase64String(_ base64String: String?) -> SpeechModel? { - guard let base64Valid = base64String else { - return nil - } - let model = SpeechModel() - model.path = base64Valid - model.fileType = .base64 - return model - } - - // MARK: - Private Methods - private func loadFileData() { - guard !path.isEmpty else { - dlog("❌ SpeechModel's file path is nil") - setupDefault() - return - } - - loadState = .loading - stateChanged() - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - do { - let audioData = try Data(contentsOf: URL(fileURLWithPath: self.path), options: .mappedIfSafe) - self.player = try AVAudioPlayer(data: audioData) - self.player?.delegate = self - self.player?.prepareToPlay() - self.audioDuration = self.player?.duration ?? 0.0 - self.loadState = .complete - self.stateChanged() - } catch { - self.loadState = .failed - self.stateChanged() - } - } - } - - private func loadUrlData() { - guard !path.isEmpty else { - dlog("❌ SpeechModel's path is nil") - setupDefault() - return - } - - loadState = .loading - stateChanged() - - DispatchQueue.global().async { [weak self] in - guard let self = self, let url = URL(string: self.path) else { return } - do { - let audioData = try Data(contentsOf: url, options: .mappedIfSafe) - self.player = try AVAudioPlayer(data: audioData) - self.player?.delegate = self - self.player?.prepareToPlay() - self.audioDuration = self.player?.duration ?? 0.0 - self.loadState = .complete - self.stateChanged() - } catch { - self.loadState = .failed - self.stateChanged() - } - } - } - - private func loadBase64Data() { - guard !path.isEmpty else { - dlog("❌ SpeechModel's base64 string is nil") - setupDefault() - return - } - - loadState = .loading - stateChanged() - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - do { - guard let audioData = Data(base64Encoded: self.path) else { - DispatchQueue.main.async { - self.loadState = .failed - self.stateChanged() - } - return - } - - self.player = try AVAudioPlayer(data: audioData) - self.player?.delegate = self - self.player?.prepareToPlay() - self.audioDuration = self.player?.duration ?? 0.0 - self.loadState = .complete - self.stateChanged() - } catch { - self.loadState = .failed - self.stateChanged() - } - } - } - - private func stateChanged() { - DispatchQueue.main.async { [weak self] in - guard let self = self, let block = self.stateChangedBlock else { return } - block(self) - } - } - - private func checkPlayState() -> Bool { - return player != nil - } - - private func setupDefault() { - playState = .default - loadState = .default - audioDuration = 0.0 - stateChanged() - } - - // MARK: - Progress Update Methods - private func startProgressUpdate() { - stopProgressUpdate() - progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in - self?.updateProgress() - } - } - - private func stopProgressUpdate() { - progressTimer?.invalidate() - progressTimer = nil - } - - private func updateProgress() { - guard let player = player, player.isPlaying else { return } - let currentTime = player.currentTime - let duration = player.duration - - DispatchQueue.main.async { [weak self] in - guard let self = self, let block = self.progressChangedBlock else { return } - block(currentTime, duration) - } - } - - // MARK: - Public Methods - - func refreshPath(path: String){ - self.path = path - } - - func loadSpeechFile() { - guard !path.isEmpty else { - setupDefault() - return - } - - guard loadState != .loading else { return } - - if player != nil { - loadState = .complete - stateChanged() - return - } - - switch fileType { - case .file: - loadFileData() - case .url: - loadUrlData() - case .base64: - loadBase64Data() - } - } - - func startPlay() { - guard checkPlayState() else { return } - guard let player = player, !player.isPlaying else { return } - - do { - let session = AVAudioSession.sharedInstance() - try session.setCategory(.playAndRecord, mode: .default, options: [ - .allowBluetooth, - .defaultToSpeaker, - .allowAirPlay, - .duckOthers, - .mixWithOthers - ]) - try session.setActive(true, options: []) - player.currentTime = 0 - player.play() - playState = .playing - startProgressUpdate() - stateChanged() - } catch { - playState = .failed - stateChanged() - } - } - - func stopPlay() { - guard checkPlayState() else { - setupDefault() - return - } - if player?.isPlaying == true { - player?.stop() - playState = .complete - stateChanged() - stopProgressUpdate() - // 可选:停用会话,避免占用音频通道 - try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation]) - } - } - - func needLoadData() -> Bool { - return player == nil && loadState != .loading - } - - // MARK: - AVAudioPlayerDelegate - func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - dlog("audioPlayerDidFinishPlaying") - playState = .complete - stateChanged() - stopProgressUpdate() - } - - func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { - dlog("audioPlayerDecodeErrorDidOccur") - playState = .failed - stateChanged() - stopProgressUpdate() - } - - // MARK: - Deinit - deinit { - print("♻️ SpeechModel dealloc") - if player?.isPlaying == true { - player?.stop() - } - stopProgressUpdate() - } -} - -/* - 使用示例: - - // 创建 SpeechModel 实例 - let speechModel = SpeechModel.modelWith(path: "audio_url")! - - // 设置播放进度更新回调 - speechModel.progressChangedBlock = { currentTime, totalDuration in - let progress = currentTime / totalDuration - print("播放进度: \(Int(currentTime))s / \(Int(totalDuration))s (\(Int(progress * 100))%)") - } - - // 加载音频文件 - speechModel.loadSpeechFile() - - // 开始播放 - speechModel.startPlay() - - // 获取音频总时长 - let duration = speechModel.audioDuration - print("音频总时长: \(duration) 秒") - - // 停止播放 - speechModel.stopPlay() - */ diff --git a/crush/Crush/Src/Components/Audio/TimerModel.swift b/crush/Crush/Src/Components/Audio/TimerModel.swift deleted file mode 100644 index 053d142..0000000 --- a/crush/Crush/Src/Components/Audio/TimerModel.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// TimerModel.swift -// Crush -// -// Created by Leon on 2025/8/1. -// - -import Foundation - -class TimerModel { - // MARK: - Properties - private var timer: DispatchSourceTimer? - private var isSuspended: Bool = false - var timerBlock: (() -> Void)? - - // MARK: - Lazy Timer - private func createTimer() -> DispatchSourceTimer { - let timer = DispatchSource.makeTimerSource(queue: .global()) - timer.schedule(deadline: .now(), repeating: .milliseconds(100)) // 0.1s interval - timer.setEventHandler { [weak self] in - self?.timerBlock?() - } - return timer - } - - // MARK: - Public Methods - func startTimer() { - if !isSuspended, timer != nil { return } - isSuspended = false - if timer == nil { - timer = createTimer() - } - timer?.resume() - //dlog("🔥: resume once") - } - - func pauseTimer() { - guard let timer = timer, !isSuspended else { return } - isSuspended = true - timer.suspend() - //dlog("🔥: suspend once") - } - - // MARK: - Deinit - deinit { - dlog("📻 audio timer dealloc") - if let timer = timer { - if isSuspended { - startTimer() // Resume to allow cancellation - } - timer.cancel() - self.timer = nil - } - } -} diff --git a/crush/Crush/Src/Components/Base/CLBaseGridController.swift b/crush/Crush/Src/Components/Base/CLBaseGridController.swift deleted file mode 100644 index 88d1a21..0000000 --- a/crush/Crush/Src/Components/Base/CLBaseGridController.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// CLBaseGridController.swift -// Crush -// -// Created by Leon on 2025/7/24. -// - -import UIKit - -class CLBaseGridController: CLBaseViewController,UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{ - // MARK: - Properties - - var collectionView: UICollectionView! - var layout: UICollectionViewFlowLayout! - var page: Int = 1 - var pageSize: Int = 10 - var datas: [Any] = [] - - var listViewDidScrollCallback: ((UIScrollView) -> Void)? - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - initData() - initUI() - } - - // MARK: - Setup - - private func initData() { - page = 1 - pageSize = 10 - datas = [] - } - - private func initUI() { - layout = UICollectionViewFlowLayout() - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 0 - - collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.delegate = self - collectionView.dataSource = self - collectionView.backgroundColor = .clear - collectionView.showsVerticalScrollIndicator = true - collectionView.showsHorizontalScrollIndicator = false - - view.addSubview(collectionView) - collectionView.snp.makeConstraints { make in - make.top.equalTo(navigationView.snp.bottom) - make.leading.trailing.bottom.equalTo(view) - } - } - - // MARK: - Public Methods - - func addRefreshHeaderFooter() { - addRefreshHeader() - addRefreshFooter() - } - - func addRefreshHeader() { - RefreshHeaderAnimator { [weak self] in - self?.loadNewData() - }.link(to: collectionView) - } - - func addRefreshFooter() { - RefreshFooterAnimator { [weak self] in - self?.loadMoreData() - }.link(to: collectionView) - } - - func removeRefreshHeader() { - collectionView.mj_header = nil - } - - func removeRefreshFooter() { - collectionView.mj_footer = nil - } - - func loadNewData() { - page = 1 - loadData() - } - - func loadMoreData() { - page += 1 - loadData() - } - - func loadData() { - // To be implemented by subclasses - } - - func scrollCurrentContentTop() { - collectionView.setContentOffset(.zero, animated: true) - } - - // MARK: - UICollectionViewDataSource - - func numberOfSections(in collectionView: UICollectionView) -> Int { - return 1 - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) - return cell - } - - // MARK: - UICollectionViewDelegate - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - // To be implemented by subclasses - } - - // MARK: - UICollectionViewDelegateFlowLayout - -// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { -// // Default size, to be overridden by subclasses -// return CGSize(width: collectionView.frame.width, height: 72) -// } - - // MARK: - UIScrollViewDelegate - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - listViewDidScrollCallback?(scrollView) - } -} diff --git a/crush/Crush/Src/Components/Base/CLBaseTableController.swift b/crush/Crush/Src/Components/Base/CLBaseTableController.swift deleted file mode 100644 index c8e682c..0000000 --- a/crush/Crush/Src/Components/Base/CLBaseTableController.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// CLBaseTableController.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit -import SnapKit - -class CLBaseTableController: CLBaseViewController, UITableViewDelegate, UITableViewDataSource { - // MARK: - Properties - var tableView: UITableView! - var page: Int = 1 - var pageSize: Int = 10 - var datas: [Any] = [] - - var listViewDidScrollCallback: ((UIScrollView) -> Void)? - - // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - initData() - initUI() - } - - // MARK: - Setup - private func initData() { - page = 1 - pageSize = 10 - datas = [] - } - - private func initUI() { - tableView = UITableView(frame: .zero, style: .plain) - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.estimatedRowHeight = 80 - - tableView.sectionHeaderHeight = 0 - tableView.sectionFooterHeight = 0 - tableView.estimatedSectionHeaderHeight = 0 - tableView.estimatedSectionFooterHeight = 0 - - tableView.backgroundColor = .clear - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIWindow.safeAreaBottom + 16, right: 0) - tableView.contentInsetAdjustmentBehavior = .never - if #available(iOS 15.0, *) { - tableView.sectionHeaderTopPadding = 0 - } - view.addSubview(tableView) - tableView.snp.makeConstraints { make in - make.leading.trailing.top.bottom.equalTo(view) - } - } - - // MARK: - Public Methods - func addRefreshHeaderFooter() { - addRefreshHeader() - addRefreshFooter() - } - - func addRefreshHeader() { - RefreshHeaderAnimator { [weak self] in - self?.loadNewData() - }.link(to: tableView) - } - - func addRefreshFooter() { - RefreshFooterAnimator { [weak self] in - self?.loadMoreData() - }.link(to: tableView) - } - - func removeRefreshHeader() { - tableView.mj_header = nil - } - - func removeRefreshFooter() { - tableView.mj_footer = nil - } - - func loadNewData() { - page = 1 - loadData() - } - - func loadMoreData() { - page += 1 - loadData() - } - - func loadData() { - // To be implemented by subclasses - } - - func scrollCurrentContentTop() { - tableView.setContentOffset(.zero, animated: true) - } - - // MARK: - UITableViewDataSource - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return datas.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .default, reuseIdentifier: "cell") - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - listViewDidScrollCallback?(scrollView) - } -} diff --git a/crush/Crush/Src/Components/Base/CLBaseViewController.swift b/crush/Crush/Src/Components/Base/CLBaseViewController.swift deleted file mode 100644 index 842bccf..0000000 --- a/crush/Crush/Src/Components/Base/CLBaseViewController.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// CLBaseViewController.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import SnapKit -import UIKit - -class CLBaseViewController: UIViewController { - // MARK: - Configs - - class var shouldPresentThisVc: Bool { - return false - } - - lazy var navigationView: NavigationView = { - let view = NavigationView() - view.backButton.isHidden = true - return view - }() - - override var title: String? { - didSet { - navigationView.title = title - } - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - - override var shouldAutorotate: Bool { - return false - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait - } - - var isDisplaying: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .c.cbd - modalPresentationStyle = .fullScreen - addNavigationView() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if type(of: self).shouldPresentThisVc { - navigationView.setupBackButtonCloseIcon() - navigationView.backButton.isHidden = false - navigationView.backButton.removeTarget(nil, action: nil, for: .touchUpInside) - navigationView.backButton.addTarget(self, action: #selector(tapNaviClose), for: .touchUpInside) - } else if presentingViewController != nil && navigationController?.viewControllers.count == 1{ - navigationView.setupBackButtonCloseIcon() - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - isDisplaying = true - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - isDisplaying = false - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - - func disabledFullScreenPan() { - if let navc: CLNavigationController = navigationController as? CLNavigationController { - navc.disabledFullScreenPan() - } - self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false - } - - /// 开启全屏 - func enabledFullScreenPan() { - if let navc: CLNavigationController = navigationController as? CLNavigationController { - navc.enabledFullScreenPan() - } - self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true - } - - @objc func tapNaviClose() { - dismiss(animated: true) - } - - deinit { - print("♻️ \(classForCoder)") - - continueDialogIfHaveWhenDealloc() - } - - private func continueDialogIfHaveWhenDealloc() { - guard let subControllers = navigationController?.viewControllers else { return } - let popControllers = subControllers.filter { $0 is BaseMaskPopDialogController } as! [BaseMaskPopDialogController] - - if !popControllers.isEmpty { - for per in popControllers { - if !DialogPopUpManager.shared.toBeHandleMaskPopControllers.contains(per) { - DialogPopUpManager.shared.toBeHandleMaskPopControllers.append(per) - } - } - DialogPopUpManager.shared.delayToContinueDialog() - } - } -} - -extension CLBaseViewController { - public func addNavigationView() { - view.addSubview(navigationView) - navigationView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.height.equalTo(UIWindow.navBarTotalHeight) - } - } - - public func hiddenBackButton(of isHidden: Bool) { - navigationView.backButton.isHidden = isHidden - } - - public func close(dismissFirst:Bool = false, completion:(()->Void)? = nil) { - if let vcs = navigationController?.viewControllers, vcs.count > 1 { - if presentingViewController != nil && dismissFirst{ - dismiss(animated: true, completion: completion) - }else{ - navigationController?.popViewController(animated: true) - } - } else { - dismiss(animated: true, completion: completion) - } - } - - public func presentNaviRootVc(vc: UIViewController, animated: Bool = true){ -// if let clBaseVc = vc as? CLBaseViewController{ // 转移到viewWillAppear中 -// clBaseVc.navigationView.setupBackButtonCloseIcon() -// } - - let navc = CLNavigationController(rootViewController: vc) - navc.modalPresentationStyle = .fullScreen - present(navc, animated: animated) - } - -} - -class CLViewController: CLBaseViewController { - var container: Container { view as! Container } - - override func loadView() { - super.loadView() - if view is Container { - return - } - - view = Container() - if let realContainer = view as? CLContainer { - realContainer.navigationView = navigationView - } -// if Container.self is CLContainer.Type{ -// view = CLContainer(navigationView: navigationView) -// }else{ -// view = Container() -// } - } -} diff --git a/crush/Crush/Src/Components/Base/CLNavigationController.swift b/crush/Crush/Src/Components/Base/CLNavigationController.swift deleted file mode 100644 index 613c581..0000000 --- a/crush/Crush/Src/Components/Base/CLNavigationController.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// CLNavigationController.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import UIKit - -protocol NavigationControllerDelegate: NSObjectProtocol { - func popGestureRecognizerShouldBegin() -> Bool -} - -extension NavigationControllerDelegate { - func popGestureRecognizerShouldBegin() -> Bool { - return true - } -} - -class CLNavigationController: UINavigationController { - - weak var popDelegate: NavigationControllerDelegate? - - private(set) var pan: UIPanGestureRecognizer! - - override func viewDidLoad() { - super.viewDidLoad() - navigationBar.isHidden = true - modalPresentationStyle = .fullScreen - addFullScreenPan() - } - - override func pushViewController(_ viewController: UIViewController, animated: Bool) { - if viewControllers.count > 0 { - if viewController is CLBaseViewController { - let vc = viewController as! CLBaseViewController - vc.hiddenBackButton(of: false) - } - viewController.hidesBottomBarWhenPushed = true - } - // 如果viewController已经在navigationStack中,则不进行push - if viewControllers.contains(viewController) { - return - } - - super.pushViewController(viewController, animated: animated) - } - - override open var childForStatusBarStyle: UIViewController? { - return topViewController - } - - private func addFullScreenPan() { - // 1.获取系统的Pop手势 - guard let systemGes = interactivePopGestureRecognizer else { return } - // 2.获取手势添加到的View中 - guard let gesView = systemGes.view else { return } - // 3.取出target - let targets = systemGes.value(forKey: "_targets") as? [NSObject] - guard let targetObjc = targets?.first else { return } - guard let target = targetObjc.value(forKey: "target") else { return } - // 4.取出action - let action = Selector(("handleNavigationTransition:")) - // 5. 创建自己的pan手势 - pan = UIPanGestureRecognizer() - gesView.addGestureRecognizer(pan) - pan.addTarget(target, action: action) - pan.delegate = self - } - - /// 关闭全屏 保留系统手势 - func disabledFullScreenPan() { - pan.isEnabled = false - interactivePopGestureRecognizer?.delegate = self - } - - /// 开启全屏 - func enabledFullScreenPan() { - pan.isEnabled = true - } - - /// 关闭全屏 和 系统 - func disabledPopGesture() { - pan.isEnabled = false - interactivePopGestureRecognizer?.isEnabled = false - } - - /// 开启全屏 和 系统 - func enabledPopGesture() { - pan.isEnabled = true - interactivePopGestureRecognizer?.isEnabled = true - } -} - -extension CLNavigationController: UIGestureRecognizerDelegate { - func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if viewControllers.count <= 1 { - return false - } - if value(forKey: "_isTransitioning") as? Bool ?? false { - return false - } - let translation = gestureRecognizer.location(in: gestureRecognizer.view) - if translation.x <= 0 { - return false - } - - if let popDelegate = popDelegate { - return popDelegate.popGestureRecognizerShouldBegin() - } - return true - } -} - - -// MARK: - 解决全屏滑动时的手势冲突 - -extension UIScrollView: @retroactive UIGestureRecognizerDelegate { - // 当UIScrollView在水平方向滑动到第一个时,默认是不能全屏滑动返回的,通过下面的方法可实现其滑动返回。 - override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if panBack(gestureRecognizer: gestureRecognizer) { - return false - } - return true - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool { - if panBack(gestureRecognizer: gestureRecognizer) { - return true - } - return false - } - - func panBack(gestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer == panGestureRecognizer { - let point = panGestureRecognizer.translation(in: self) - let state = gestureRecognizer.state - - // 设置手势滑动的位置距屏幕左边的区域 - let locationDistance = UIScreen.main.bounds.size.width - - if state == UIGestureRecognizer.State.began || state == UIGestureRecognizer.State.possible { - let location = gestureRecognizer.location(in: self) - if point.x > 0, location.x < locationDistance, contentOffset.x <= 0 { - return true - } - } - } - return false - } -} diff --git a/crush/Crush/Src/Components/Base/CLTabRootController.swift b/crush/Crush/Src/Components/Base/CLTabRootController.swift deleted file mode 100644 index aa7aec2..0000000 --- a/crush/Crush/Src/Components/Base/CLTabRootController.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// CLTabRootController.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -class CLTabRootController: CLViewController{ - private var bgTopIv: UIImageView! - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - // 创建拖拽卡片容器 - bgTopIv = { - let v = UIImageView() - v.image = UIImage(named: "meet_top_bg") - view.insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(200/393.0) - } - return v - }() - } - - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - -} diff --git a/crush/Crush/Src/Components/Base/H5BaseViewController.swift b/crush/Crush/Src/Components/Base/H5BaseViewController.swift deleted file mode 100755 index 38ed637..0000000 --- a/crush/Crush/Src/Components/Base/H5BaseViewController.swift +++ /dev/null @@ -1,489 +0,0 @@ -// -// H5BaseViewController.swift -// E-Wow -// -// Created by dong on 2021/1/5. -// - -// import Alamofire -// import CryptoSwift -import UIKit -import URLNavigator -import WebKit - -class H5BaseViewController: CLBaseViewController { - public var navTitleShow: Bool = true - - var targetUrl: URL! = URL(string: "") - var lastProgress: Double! = 0 - - private var observeRegister: Bool = false - - private let schemes = ["route", "closeWebview", "getUserInfo", "setLoading", "request", "modal", "share", "init", "openBrowser", "getAppVersion", "sendTrack"] - - lazy var webView: WKWebView! = { - let config = WKWebViewConfiguration() - let preferences = WKPreferences() - preferences.javaScriptCanOpenWindowsAutomatically = true - config.preferences = preferences - config.allowsInlineMediaPlayback = true - config.mediaTypesRequiringUserActionForPlayback = .video - - let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 320, height: 480), configuration: config) - // 以下3行修改webView颜色是有效的✅ - webView.backgroundColor = .c.cbd - webView.scrollView.backgroundColor = .c.cbd - webView.isOpaque = false - let jsFilePath = Bundle.main.path(forResource: "webview", ofType: "js") - if let jshtml = try? String(contentsOfFile: jsFilePath!, encoding: .utf8) { - let script = WKUserScript(source: jshtml, injectionTime: .atDocumentStart, forMainFrameOnly: true) - webView.configuration.userContentController.addUserScript(script) - } - - return webView - }() - - lazy var progressView: UIProgressView = { - let progressView = UIProgressView(frame: .zero) - progressView.progress = 0 - progressView.setProgress(0, animated: false) - navigationView.addSubview(progressView) - progressView.snp.makeConstraints { make in - make.left.right.bottom.equalToSuperview() - make.height.equalTo(1) - } - progressView.progressTintColor = .c.cpn//.purple - progressView.trackTintColor = .clear - return progressView - }() - - lazy var closeButton: UIButton = { - let button = UIButton(type: .custom) - button.setImage(R.image.icon_close_20(), for: .normal) - button.addTarget(self, action: #selector(tapNaviCloseBtn), for: .touchUpInside) - button.isHidden = true - navigationView.leftStackH.addArrangedSubview(button) - button.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 44, height: 44)) - } - return button - }() - - override func viewDidLoad() { - super.viewDidLoad() - -// view.backgroundColor = .white -// webView.backgroundColor = .white - webView.backgroundColor = .c.cbd - webView.navigationDelegate = self - - view.addSubview(webView) - webView.snp.makeConstraints { make in - make.bottom.right.left.equalTo(self.view) - make.top.equalTo(navigationView.snp.bottom) - } - - navigationView.backButton.removeTarget(navigationView, action: .none, for: .touchUpInside) - navigationView.backButton.addTarget(self, action: #selector(tapNaviBackBtn), for: .touchUpInside) - - setupBaseEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - for (_, per) in schemes.enumerated() { - webView.configuration.userContentController.add(self, name: per) - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - for (_, per) in schemes.enumerated() { - webView.configuration.userContentController.removeScriptMessageHandler(forName: per) - } - } - - public func loadURL(url: URL) { - /* - if let lan = Languages.preferedLans.first { - let queryItem = URLQueryItem(name: "lang", value: lan.rawValue) - if var compoments = URLComponents(url: url, resolvingAgainstBaseURL: true){ - if let items = compoments.queryItems, items.count > 0{ - compoments.queryItems?.append(queryItem) - }else{ - compoments.queryItems = [queryItem] - } - let afterUrl = compoments.url - targetUrl = afterUrl - } - dlog("h5 path with language: \(String(describing: targetUrl))") - } else { - targetUrl = url - } - */ - targetUrl = url - } - - func setupBaseEvents() { - weak var wself = self - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - guard let url = self.targetUrl else { - assert(false) - return - } - - let request = NSMutableURLRequest(url: url) - wself?.webView.load(request as URLRequest) - } - - webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) - webView.addObserver(self, forKeyPath: "title", options: .new, context: nil) - observeRegister = true - progressView.setProgress(0, animated: false) - } - - deinit { - webView.configuration.userContentController.removeAllUserScripts() - if observeRegister { - webView.removeObserver(self, forKeyPath: "title") - webView.removeObserver(self, forKeyPath: "estimatedProgress") - } - } - - func handleInit(msg: JSSDKMessage) { - // wait to override the func - } -} - -extension H5BaseViewController { - @objc func tapNaviBackBtn() { - if webView.canGoBack { - webView.goBack() - } else { - close() - } - } - - @objc func tapNaviCloseBtn() { - close() - } -} - -// MARK: - Helper - -extension H5BaseViewController { - func stopHtmlVoice() { - // ... - let jsaudio = "var vids = document.getElementsByTagName('audio'); for( var i = 0; i < vids.length; i++ ){vids.item(i).pause()}" - webView.evaluateJavaScript(jsaudio, completionHandler: nil) - } - - static func clearCache() { -// let dataStore = WKWebsiteDataStore.default() -// dataStore.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), completionHandler: { records in -// for record in records { -// WKWebsiteDataStore.default().removeData(ofTypes: record.dataTypes, for: [record], completionHandler: { -// print("♻️✅ Webview cache clear successfully\(record)") -// }) -// } -// }) - - let websiteDataTypes: Set = [ - WKWebsiteDataTypeDiskCache, - WKWebsiteDataTypeMemoryCache, - WKWebsiteDataTypeLocalStorage, - WKWebsiteDataTypeWebSQLDatabases, - WKWebsiteDataTypeIndexedDBDatabases - ] - - // 从 1970 开始,意味着清除所有历史数据 - let dateFrom = Date(timeIntervalSince1970: 0) - - WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: dateFrom) { - print("WebView data cleared") - } - } - - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - if keyPath == "title" { - if navTitleShow { - navigationView.titleLabel.text = webView.title - } - } else if keyPath == "estimatedProgress" { - updateProgress(progress: webView.estimatedProgress) - } else { - } - } - - func updateProgress(progress: Double) { - progressView.alpha = 1 - // dlog("progress : \(progress)") - if progress > lastProgress { - progressView.setProgress(Float(progress), animated: true) - } else { - progressView.setProgress(Float(progress), animated: false) - } - - lastProgress = progress - - if progress >= 1 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.progressView.alpha = 0 - self?.progressView.setProgress(0, animated: false) - self?.lastProgress = 0 - } - } - } -} - -// MARK: - Handle Event - -extension H5BaseViewController { - func handleRoute(msg: JSSDKMessage) { - if let uri = msg.uri?.urlValue { - navigator.open(uri) - } - } - - func handleRequest(msg: JSSDKMessage) { -// let requestUri = msg.uri -// let params = msg.params - // for h5自主调用,静默请求...成功后执行成功回调,失败后执行error方法。 - - guard let uri = msg.uri else { - return - } - - var requestAny: AnyCodable = AnyCodable(value: [:]) - - let token = UserCore.shared.token - if !token.isEmpty { - do { -// let body = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted) -// let str = String(data: body, encoding: .utf8) - var str = "" - if let dict = msg.params, let data2: Data = try? JSONSerialization.data(withJSONObject: dict, options: []) { - let objcAnyCodable = try! JSONDecoder().decode(AnyCodable.self, from: data2) - let backToJson = try! JSONEncoder().encode(objcAnyCodable) - let jsonString = String(bytes: backToJson, encoding: .utf8)! - str = jsonString - } - -// let key = (token + "AHkt5aUUtO6HZPid").md5().uppercased() -// let aes = try AES(key: key, iv: "HBB4UO5kEmM4169Z") -// let encrypted = try aes.encrypt(str.bytes) -// let result = encrypted.toBase64() -// let dic = ["key": result] -// dlog("⚠️ 加密前参数:\(str) \n⚠️ 加密结果:\(dic)") -// if let data3: Data = try? JSONSerialization.data(withJSONObject: dic, options: []) { -// let objcAnyCodable = try! JSONDecoder().decode(AnyCodable.self, from: data3) -// requestAny = objcAnyCodable -// } - } catch { - } - } else { - if let dict = msg.params, let data2: Data = try? JSONSerialization.data(withJSONObject: dict, options: []), let objcAnyCodable = try? JSONDecoder().decode(AnyCodable.self, from: data2) { - requestAny = objcAnyCodable - } - } - -// let headers = HTTPHeaders(APIConfig.apiHeaders()!) - - Hud.showIndicator() - dlog("☁️h5 request:\(uri) params: \(requestAny)") -// AF.request(uri, method: .post, parameters: requestAny, encoder: JSONParameterEncoder.default, headers: headers, interceptor: nil, requestModifier: nil).responseString { [weak self] response in -// // dlog("response: \(response)") -// self?.view.hideToastActivity() -// switch response.result { -// case let .success(model): -// guard let ltResponse: ResponseData = ResponseData>.deserialize(from: model) else { -// return -// } -// -// if ltResponse.status == "OK" { -// if let ltArrayResponse = ResponseData>>.deserialize(from: model), let jsonDict = ltArrayResponse.content { -// if let data = try? JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted), let str = String(data: data, encoding: .utf8) { -// let jsonString = str // content2.toJSONString() ?? "" -// let js = "\(msg.success ?? "")((\(jsonString)))" -// dlog("✅ success call js:\(js)") -// self?.webView.evaluateJavaScript(js, completionHandler: { _, error in -// if error != nil { -// dlog("❌ exec js error: \(error?.localizedDescription ?? "")") -// } -// }) -// return -// } -// -// } else if let content = ltResponse.content { -// let jsonString = content.toJSONString() -// let js = "\(msg.success ?? "")((\(jsonString ?? "")))" -// dlog("✅ success call js:\(js)") -// self?.webView.evaluateJavaScript(js, completionHandler: { _, error in -// if error != nil { -// dlog("❌ exec js error: \(error?.localizedDescription ?? "")") -// } -// }) -// return -// } -// -// let js = "\(msg.success ?? "")()" -// dlog("✅ success call js no content:\(js)") -// self?.webView.evaluateJavaScript(js, completionHandler: { _, error in -// if error != nil { -// dlog("❌ exec js error: \(error?.localizedDescription ?? "")") -// } -// }) -// } else { -// // --- 接口错误 -// let js = "\(msg.error ?? "")((\(model)))" -// dlog("❌ api error: \(js)") -// self?.webView.evaluateJavaScript(js, completionHandler: nil) -// } -// -// break -// default: -// // --- 网络等错误 -// UIWindow.key?.makeToast(R.string.localizable.internet_connect_failed.localized()) -// let js = "\(msg.error ?? "")()" -// dlog("❌ api network error: \(js)") -// self?.webView.evaluateJavaScript(js, completionHandler: nil) -// break -// } -// } - } - - func handleLoading(msg: JSSDKMessage) { - if msg.status { - UIWindow.key?.makeToastActivity() - - } else { - UIWindow.key?.hideToastActivity() - } - } - - func handleGetUserInfo(msg: JSSDKMessage) { - } - - func handleModal(msg: JSSDKMessage) { - // 取决于msg.type. 暂无 - } - - func handleOpenBrowser(msg: JSSDKMessage) { - if let uri = msg.uri, uri.count > 0, let url = URL(string: uri) { - UIApplication.shared.open(url, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: false], completionHandler: nil) - } - } - - func handleGetAppversion(msg: JSSDKMessage) { - let version = Bundle.appVersion - let dict = ["version": version] - let data1 = try? JSONSerialization.data(withJSONObject: dict, options: []) - let dictJs = String(data: data1!, encoding: .utf8) ?? "" - // dict.toJSONString() ?? "" - let js = "\(msg.success ?? "")((\(dictJs)))" - dlog("✅ success call js:\(js)") - webView.evaluateJavaScript(js, completionHandler: { _, error in - if error != nil { - dlog("❌ exec js error: \(error?.localizedDescription ?? "")") - } - }) - } - - private func handleDealSendTrack(msg: JSSDKMessage) { - if let trackName = msg.name { - var params = msg.params ?? [:] - if let uid = UserCore.shared.user?.userId { - // params.updateValue(uid, forKey: "userId") - } - // AppAnalytics.commonRecord(trackName, parameters: params) - } - } -} - -// MARK: - 🔥 WKScriptMessageHandler - -extension H5BaseViewController: WKScriptMessageHandler { - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - dlog("☁️☁️ h5 message name: \(message.name) message body: \(message.body)") - let msgName = message.name - guard let body = message.body as? String else { - // elog("h5 message failed! : \(msgName)") - return - } - - guard let msg = CodableHelper.decode(JSSDKMessage.self, from: body) else { - return - } - - if msgName == "route" { - msg.msgName = msgName - handleRoute(msg: msg) - } else if msgName == "request" { - msg.msgName = msgName - handleRequest(msg: msg) - - } else if msgName == "setLoading" { - msg.msgName = msgName - handleLoading(msg: msg) - - } else if msgName == "closeWebview" { - close() - } else if msgName == "getUserInfo" { - msg.msgName = msgName - handleGetUserInfo(msg: msg) - - } else if msgName == "modal" { - msg.msgName = msgName - handleModal(msg: msg) - - } else if msgName == "share" { - // to do. - } else if msgName == "init" { - handleInit(msg: msg) - - } else if msgName == "openBrowser" { - msg.msgName = msgName - handleOpenBrowser(msg: msg) - - } else if msgName == "getAppVersion" { - msg.msgName = msgName - handleGetAppversion(msg: msg) - - } else if msgName == "sendTrack" { - msg.msgName = msgName - handleDealSendTrack(msg: msg) - - } else { - // Please upgrade to the latest version - dlog("🛎Please upgrade to the latest version,Unsupported protocol.") - } - } -} - -// MARK: - WKNavigationDelegate` - -extension H5BaseViewController: WKNavigationDelegate { - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - guard let urlRequest = navigationAction.request.url?.absoluteString.removingPercentEncoding else { - decisionHandler(.cancel) - return - } - dlog("☁️ webview load: \(urlRequest) ☁️") - if urlRequest.hasPrefix(AppConst.schemePrefix) { - navigator.open(urlRequest) - - } else if urlRequest.hasPrefix("mailto:") { // open system to send email. - if let url = URL(string: urlRequest) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - } - - decisionHandler(.allow) - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - dlog("didFinish 🔥🔥🔥\(String(describing: webView.url))") - - closeButton.isHidden = !webView.canGoBack - } -} diff --git a/crush/Crush/Src/Components/Base/H5Controller.swift b/crush/Crush/Src/Components/Base/H5Controller.swift deleted file mode 100755 index 40acea3..0000000 --- a/crush/Crush/Src/Components/Base/H5Controller.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// H5Controller.swift -// E-Wow -// -// Created by dong on 2021/1/6. -// - -import UIKit -import WebKit - -/** - 导航栏默认显示,状态栏默认深色 - */ -class H5Controller: H5BaseViewController { - private var bgColor: UIColor = UIColor(hex: "#211A2B")//.purple//.white - private var background = "#211A2B" - private var naviTitleIgnoreAlpha = false - - // status - private var h5StatusBarStyle: UIStatusBarStyle = .lightContent - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViewsByBgColor(alpha: 1) - navigationView.backButton.isHidden = false - -// navigationView.backButton.setImage(R.image.nav_back(), for: .normal) -// webView.snp.remakeConstraints { make in -// make.top.right.left.bottom.equalTo(self.view) -// } -// if #available(iOS 13.0, *) { -// h5StatusBarStyle = .darkContent -// }else{ -// h5StatusBarStyle = .default -// } -// setNeedsStatusBarAppearanceUpdate() - - view.bringSubviewToFront(navigationView) - - webView.scrollView.delegate = self - } - - private func setupViewsByBgColor(alpha: CGFloat) { - navigationView.backgroundColor = bgColor.withAlphaComponent(alpha) - navigationView.titleLabel.alpha = naviTitleIgnoreAlpha ? 1 : alpha - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return h5StatusBarStyle - } - - override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - super.webView(webView, didFinish: navigation) - dlog("☁️webview didFinish: \(webView.url?.absoluteString ?? "null")") - guard let urlPath = webView.url?.absoluteString else { - return - } - - adaptPageNavi(uri: urlPath, didFinish: true) - } - - override func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - super.webView(webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) - - guard let urlRequest = navigationAction.request.url?.absoluteString.removingPercentEncoding else { - return - } - - adaptPageNavi(uri: urlRequest) - } - - private func adaptPageNavi(uri: String?, didFinish: Bool = false) { - guard let uriStr = uri else { - return - } - - if uriStr.contains("activity/shoppingMall") { - bgColor = .hexString("F04545") - navigationView.titleLabel.textColor = .white - naviTitleIgnoreAlpha = true - setupViewsByBgColor(alpha: 0) - if didFinish { - navigationView.backButton.setImage(R.image.nav_back_white(), for: .normal) - } - webView.evaluateJavaScript("document.body.style.backgroundColor=\"#F04545\"") { _, _ in - } - } else { -// navigationView.backButton.setImage(R.image.nav_back(), for: .normal) - webView.evaluateJavaScript("document.body.style.backgroundColor=\"\(background)\"") { _, _ in - } - } - } - - // MARK: - override - - override func handleInit(msg: JSSDKMessage) { - super.handleInit(msg: msg) - if let header = msg.header { -// if let fontColor = header.fontColor { -// navigationView.titleLabel.textColor = UIColor.hexString(fontColor) -// if fontColor == "#FFFFFF" { -// navigationView.backButton.setImage(R.image.nav_back_white(), for: .normal) -// } else { -// navigationView.backButton.setImage(R.image.nav_back(), for: .normal) -// } -// } - if let background = header.background { - self.background = background - bgColor = UIColor.hexString(background) - setupViewsByBgColor(alpha: 1) - } - if header.transparent { - setupViewsByBgColor(alpha: 0) - webView.snp.remakeConstraints { make in - make.top.right.left.bottom.equalTo(self.view) - } - h5StatusBarStyle = .lightContent - setNeedsStatusBarAppearanceUpdate() - } else { - webView.snp.remakeConstraints { make in - make.right.left.bottom.equalTo(self.view) - make.top.equalTo(navigationView.snp.bottom) - } - if #available(iOS 13.0, *) { - h5StatusBarStyle = .darkContent - } else { - h5StatusBarStyle = .default - } - setNeedsStatusBarAppearanceUpdate() - } - } - } -} - -extension H5Controller: UIScrollViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - // dlog("offset:\(scrollView.contentOffset.y)") - if scrollView.contentOffset.y <= 60 && scrollView.contentOffset.y >= 0 { - let aspect = scrollView.contentOffset.y / 60.0 - setupViewsByBgColor(alpha: aspect) - } else if scrollView.contentOffset.y > 60 { - setupViewsByBgColor(alpha: 1) - } else { - setupViewsByBgColor(alpha: 0) - } - } -} diff --git a/crush/Crush/Src/Components/IconFont/IconFontCode.swift b/crush/Crush/Src/Components/IconFont/IconFontCode.swift deleted file mode 100644 index 9566103..0000000 --- a/crush/Crush/Src/Components/IconFont/IconFontCode.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// IconFontCode.swift -// OCIconFontDemo -// -// Created by lym on 2022/8/31. -// - -import Foundation - -/// IconFont字体对应Unicode-Int类型的 -@objc public enum IconCode: UInt32 { - case shield = 0xe6fa - case audits = 0xe6fb - case postRecommendFill = 0xe6f9 - case postNotrecommendFill = 0xe6f8 - case voiceMsg = 0xe652 - case clear = 0xe657 - case prompt = 0xe66f - case statistics = 0xe66e - case sortAz = 0xe66d - case orderlobby = 0xe6f7 - case link = 0xe6f6 - case minimize = 0xe66c - case iconWatchtogether = 0xe6f5 - case seat = 0xe6f4 - case muteAll = 0xe66b - case entranceSound = 0xe6f3 - case sort = 0xe66a - case vipNocolor = 0xe6f2 - case purchaseorder = 0xe6f0 - case placeorder = 0xe6f1 - case connect = 0xe669 - case tag = 0xe668 - case card = 0xe667 - case steamDeck = 0xe666 - case android = 0xe661 - case xbox = 0xe662 - case ps = 0xe663 - case platformSwitch = 0xe664 - case pc = 0xe665 - case thread = 0xe6ef - case iconPostNotrecommend = 0xe6ee - case couponBorder = 0xe660 - case date = 0xe6ed - case micRequestBorder = 0xe65e - case micRequest = 0xe65f - case iconPublicBorder = 0xe65c - case iconPrivateBorder = 0xe65d - case female = 0xe659 - case nonconforming = 0xe65a - case male = 0xe65b - case iconExclamation = 0xe6ec - case stop = 0xe658 - case language = 0xe654 - case account = 0xe655 - case imSetting = 0xe656 - case info = 0xe653 - case copy = 0xe6eb - case iconDrag = 0xe650 - case inviteToMic = 0xe6ea - case voiceLive = 0xe651 - case question = 0xe64f - case startLive = 0xe64e - case unfollow = 0xe6c8 - case following = 0xe6d4 - case breakLink = 0xe6c5 - case iconBeautySmooth = 0xe64a - case iconBeautyRedness = 0xe64b - case iconBeautySharp = 0xe64c - case iconBeautyWhitening = 0xe64d - case volumeDown = 0xe647 - case volumeMute = 0xe648 - case volumeUp = 0xe649 - case emojiAnimated = 0xe6c4 - case flip = 0xe6e6 - case micBorder = 0xe6e7 - case randomPlay = 0xe6e8 - case speakerBorder = 0xe6e9 - case backfullscreen = 0xe6ca - case blocklisk = 0xe6ce - case beauty = 0xe6d1 - case fullscreenExitFill = 0xe6d3 - case fullscreenFill = 0xe6d5 - case fans = 0xe6d6 - case fullscreenMobile = 0xe6d7 - case kick = 0xe6d8 - case listPlay = 0xe6d9 - case previous = 0xe6da - case data = 0xe6db - case giftEffect = 0xe6dc - case giftBorder = 0xe6dd - case loopPlay = 0xe6de - case notice = 0xe6df - case time = 0xe6e0 - case volume = 0xe6e1 - case playlist = 0xe6e2 - case next = 0xe6e3 - case music = 0xe6e4 - case offmic = 0xe6e5 - case pause = 0xe6c1 - case iconFullimage = 0xe646 - case iconCameraFill = 0xe645 - case vote = 0xe6c0 - case iconEmail = 0xe644 - case iconChatroomBigmenu = 0xe674 - case iconChatroomSmallmenu = 0xe675 - case chatroomMic = 0xe685 - case chatroomOn = 0xe687 - case exit = 0xe688 - case chatroomMore = 0xe68f - case iconFobidden = 0xe694 - case iconShareInvtation = 0xe6ab - case iconEntrance = 0xe6bc - case iconComunityDelete = 0xe643 - case iconComunityReply = 0xe63e - case iconCommunityLike = 0xe63f - case iconComunityReport = 0xe640 - case iconComunityTopics = 0xe641 - case iconComunityComment = 0xe642 - case iconMenuCustomer = 0xe6a4 - case iconPostCollection = 0xe6c2 - case iconPostComponent = 0xe6c3 - case iconPostRecommend = 0xe6c6 - case iconPostHighlight = 0xe6c7 - case iconPostViewers = 0xe6c9 - case iconPostChitchat = 0xe6cc - case iconPostSelfies = 0xe6cd - case iconPostOfficial = 0xe6cb - case iconOrderTip = 0xe6d0 - case iconOrderRemark = 0xe6d2 - case iconRefund = 0xe63c - case iconPost = 0xe63b - case backtop = 0xe63d - case faq = 0xe6ba - case onlineservice = 0xe6bb - case iconVip = 0xe638 - case iconLegends = 0xe639 - case iconInfluencer = 0xe63a - case iconComment = 0xe636 - case iconHelpcenter = 0xe634 - case iconSetting = 0xe635 - case iconReload = 0xe633 - case iconReset = 0xe631 - case iconAddcard = 0xe62e - case iconSend = 0xe630 - case iconServiceFill = 0xe632 - case iconService = 0xe62f - case iconRandom = 0xe62d - case iconKeyboard = 0xe62c - case iconPeople = 0xe629 - case iconTop = 0xe62b - case iconOrderFill = 0xe62a - case add = 0xe6b5 - case reduce = 0xe6b6 - case iconContact = 0xe616 - case warning = 0xe614 - case iconImPurpleface = 0xe6bd - case iconImEmoji = 0xe6bf - case iconImOther = 0xe6cf - case chatroomOff = 0xe686 - case iconImBlocked = 0xe6b9 - case iconCallMute = 0xe684 - case iconCallOpenmic = 0xe6b4 - case iconCallHangup = 0xe68d - case iconInteract = 0xe693 - case quote = 0xe6b3 - case checked = 0xe6b2 - case report = 0xe6b1 - /// 垃圾桶🗑️删除 - case iconDelete = 0xe612 - case iconPublic = 0xe698 - case iconPrivate = 0xe699 - case iconUploadimg = 0xe69c - case iconUploadclip = 0xe69d - case arrowDownBorder = 0xe6af - case arrowLeft = 0xe6b0 - case eyeOff = 0xe6ae - case eyeOn = 0xe6ad - case arrowDownFill = 0xe6ac - case arrowLeftBorder = 0xe6aa - case delete02 = 0xe628 - case arrowDownCircle = 0xe601 - case arrowRightBorder = 0xe602 - case arrowUpFill = 0xe603 - case arrowRight = 0xe604 - case chat = 0xe605 - case delete = 0xe607 - case createRoom = 0xe608 - case call = 0xe609 - case likeFill = 0xe60a - case filterFill = 0xe60b - case live = 0xe60c - case follow = 0xe60d - case play = 0xe60e - case events = 0xe60f - case more = 0xe610 - case matching = 0xe611 - case like = 0xe613 - case loading = 0xe615 - case search = 0xe617 - case filter = 0xe618 - case socialFacebook = 0xe619 - case select = 0xe61a - case shareBorder = 0xe61b - case socialInstagram = 0xe61c - case socialTwitch = 0xe61d - case voice = 0xe61e - case socialTiktok = 0xe61f - case socialGoogle = 0xe620 - case socialYoutube = 0xe621 - case socialApple = 0xe622 - case iconKfckfc = 0xe623 - case starFill = 0xe624 - case star = 0xe625 - case socialDiscord = 0xe626 - case messages = 0xe627 - -} diff --git a/crush/Crush/Src/Components/IconFont/MWIconFont.swift b/crush/Crush/Src/Components/IconFont/MWIconFont.swift deleted file mode 100644 index 4b29040..0000000 --- a/crush/Crush/Src/Components/IconFont/MWIconFont.swift +++ /dev/null @@ -1,205 +0,0 @@ -// -// MWIconFont.swift -// Test -// -// Created by MYK on 2019/5/2. -// Copyright © 2019 MYK. All rights reserved. -// - -import UIKit - -/// IconFont -@objcMembers open class MWIconFont: UIFont, @unchecked Sendable { - static func printIconCodeEnum() { - guard let fileURL = Bundle.main.url(forResource: "iconfont", withExtension: "json"), - let data = try? Data(contentsOf: fileURL), - let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let root = try? JSONDecoder().decode(IconFontRootModel.self, from: data) else { - print("Error loading or parsing JSON file") - return - } - - let list = root.glyphs - var set: [String] = [] - - for obj in list { - // Convert name to camelCase and make first character lowercase - var name = obj.name - .replacingOccurrences(of: "-", with: "_") - .replacingOccurrences(of: " ", with: "_") - - // Simple camelCase conversion (you might need a more sophisticated version) - let components = name.components(separatedBy: "_") - name = components.enumerated().map { (index, component) in - index == 0 ? component.lowercased() : component.capitalized - }.joined() - - let code = obj.unicode - let result = "case \(name) = 0x\(code)" - set.append(result) - } - - let printStr = set.joined(separator: "\n") - print("\n\(printStr)\n") - } - - private class func iconFont(_ size: CGFloat) -> UIFont? { - if size == 0.0 { - return nil - } - let iconfont = "iconfont" // 字体的名字 - loadMyCustomFont(iconfont) - return UIFont(name: iconfont, size: size) - } - - private class func loadMyCustomFont(_ name: String) { - guard let fontPath = Bundle(for: MWIconFont.self).path(forResource: name, ofType: "ttf") else { - return - } - var error: Unmanaged? - guard let data = try? Data(contentsOf: URL(fileURLWithPath: fontPath)), - let provider = CGDataProvider(data: data as CFData) - else { - return - } - if let font = CGFont(provider) { - CTFontManagerRegisterGraphicsFont(font, &error) - if error != nil { - return - } - } - } - - /// 将Int类型的转为String类型的Unicode码,eg: 0xe624 -> \u{E624} - private class func stringForIcon(_ iconInt: UInt32) -> String? { - var rawIcon = iconInt - let xPtr = withUnsafeMutablePointer(to: &rawIcon) { $0 } - return String(bytesNoCopy: xPtr, - length: MemoryLayout.size, - encoding: String.Encoding.utf32LittleEndian, - freeWhenDone: false) - } - - /// 生成IconFont - /// - /// - Parameters: - /// - iconInt: Unicode-Int类型的 eg: 0xe624 - /// - size: fontSize - /// - color: fontColor - /// - Returns: font? - @objc public class func attributedString( - fromIconInt iconInt: UInt32, - size: CGFloat, - color: UIColor?) -> NSAttributedString? - { - guard let string = stringForIcon(iconInt) else { - return nil - } - return MWIconFont.attributedString(fromIconStr: string, size: size, color: color) - } - - /// 生成IconFont - /// - /// - Parameters: - /// - iconStr: Unicode-String类型的 eg: "\u{E624}" - /// - size: fontSize - /// - color: fontColor - /// - Returns: font? - @objc public class func attributedString( - fromIconStr iconStr: String, - size: CGFloat, - color: UIColor?) -> NSAttributedString? - { - guard let font = MWIconFont.iconFont(size) else { - return nil - } - var attributes = [NSAttributedString.Key: AnyObject]() - attributes[NSAttributedString.Key.font] = font - if let color = color { - attributes[NSAttributedString.Key.foregroundColor] = color - } - return NSAttributedString(string: iconStr, attributes: attributes) - } - - /// iconfont生成image - /// - /// - Parameters: - /// - iconInt: Unicode-Int类型的 eg: 0xe624 - /// - size: 图片宽高 - /// - color: 图片颜色 - /// - Returns: image? - @objc public class func image( - fromIconInt iconInt: UInt32, - size: CGSize, - color: UIColor?, - edgeInsets: UIEdgeInsets = UIEdgeInsets.zero) -> UIImage? - { - guard let string = stringForIcon(iconInt) else { - return nil - } - return MWIconFont.image(fromIconStr: string, size: size, color: color, edgeInsets: edgeInsets) - } - - @objc public class func image( - fromIcon iconEnum: IconCode, - size: CGSize, - color: UIColor?, - edgeInsets: UIEdgeInsets = UIEdgeInsets.zero) -> UIImage? - { - guard let string = stringForIcon(iconEnum.rawValue) else { - return nil - } - return MWIconFont.image(fromIconStr: string, size: size, color: color, edgeInsets: edgeInsets) - } - - /// iconfont生成image - /// - /// - Parameters: - /// - iconStr: Unicode-String类型的 eg: "\u{E624}" - /// - size: 图片宽高 - /// - color: 图片颜色 - /// - Returns: image? - private class func image( - fromIconStr iconStr: String, - size: CGSize, - color: UIColor?, - edgeInsets: UIEdgeInsets = UIEdgeInsets.zero) -> UIImage? - { - if size == CGSize.zero { - return nil - } - let pointSize = min(size.width, size.height) - guard let aString = MWIconFont.attributedString(fromIconStr: iconStr, size: pointSize, color: color) else { - return nil - } - let mString = NSMutableAttributedString(attributedString: aString) - - var rect = CGRect(origin: CGPoint.zero, size: size) - rect.origin.y -= edgeInsets.top - rect.size.width -= edgeInsets.left + edgeInsets.right // 运算符优先级注意 - rect.size.height -= edgeInsets.top + edgeInsets.bottom - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - - let range = NSRange(location: 0, length: mString.length) - - mString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: range) - // render the attributed string as image using Text Kit - UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) - mString.draw(in: rect) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return image - } -} - -struct IconFontRootModel: Codable { - let glyphs: [IconFontModel] -} - -struct IconFontModel: Codable { - let name: String - let unicode: String -} diff --git a/crush/Crush/Src/Components/IconFont/iconfont.json b/crush/Crush/Src/Components/IconFont/iconfont.json deleted file mode 100644 index 28924c8..0000000 --- a/crush/Crush/Src/Components/IconFont/iconfont.json +++ /dev/null @@ -1,1423 +0,0 @@ -{ - "id": "4975639", - "name": "CrushLevel", - "font_family": "iconfont", - "css_prefix_text": "icon-", - "description": "", - "glyphs": [ - { - "icon_id": "45504317", - "name": "shield", - "font_class": "shield", - "unicode": "e6fa", - "unicode_decimal": 59130 - }, - { - "icon_id": "45504316", - "name": "audits", - "font_class": "audits", - "unicode": "e6fb", - "unicode_decimal": 59131 - }, - { - "icon_id": "45330662", - "name": "post_recommend_fill", - "font_class": "post_recommend_fill", - "unicode": "e6f9", - "unicode_decimal": 59129 - }, - { - "icon_id": "45330650", - "name": "post_Notrecommend_fill", - "font_class": "post_Notrecommend_fill", - "unicode": "e6f8", - "unicode_decimal": 59128 - }, - { - "icon_id": "45244863", - "name": "voice_msg", - "font_class": "voice_msg", - "unicode": "e652", - "unicode_decimal": 58962 - }, - { - "icon_id": "45244862", - "name": "clear", - "font_class": "clear", - "unicode": "e657", - "unicode_decimal": 58967 - }, - { - "icon_id": "45244861", - "name": "prompt", - "font_class": "prompt", - "unicode": "e66f", - "unicode_decimal": 58991 - }, - { - "icon_id": "44860023", - "name": "statistics", - "font_class": "statistics", - "unicode": "e66e", - "unicode_decimal": 58990 - }, - { - "icon_id": "44513036", - "name": "sort_az", - "font_class": "sort_az", - "unicode": "e66d", - "unicode_decimal": 58989 - }, - { - "icon_id": "44124600", - "name": "orderlobby", - "font_class": "orderlobby", - "unicode": "e6f7", - "unicode_decimal": 59127 - }, - { - "icon_id": "44005343", - "name": "link", - "font_class": "link", - "unicode": "e6f6", - "unicode_decimal": 59126 - }, - { - "icon_id": "43913018", - "name": "minimize", - "font_class": "minimize", - "unicode": "e66c", - "unicode_decimal": 58988 - }, - { - "icon_id": "43909016", - "name": "icon_watchtogether", - "font_class": "icon_watchtogether", - "unicode": "e6f5", - "unicode_decimal": 59125 - }, - { - "icon_id": "43695294", - "name": "seat", - "font_class": "seat", - "unicode": "e6f4", - "unicode_decimal": 59124 - }, - { - "icon_id": "43665887", - "name": "mute all", - "font_class": "mute1", - "unicode": "e66b", - "unicode_decimal": 58987 - }, - { - "icon_id": "43616417", - "name": "entrance sound", - "font_class": "a-entrancesound", - "unicode": "e6f3", - "unicode_decimal": 59123 - }, - { - "icon_id": "42073284", - "name": "sort", - "font_class": "sort", - "unicode": "e66a", - "unicode_decimal": 58986 - }, - { - "icon_id": "41246214", - "name": "purchaseorder", - "font_class": "purchaseorder", - "unicode": "e6f0", - "unicode_decimal": 59120 - }, - { - "icon_id": "41246213", - "name": "placeorder", - "font_class": "placeorder", - "unicode": "e6f1", - "unicode_decimal": 59121 - }, - { - "icon_id": "41066355", - "name": "connect", - "font_class": "connect", - "unicode": "e669", - "unicode_decimal": 58985 - }, - { - "icon_id": "41045990", - "name": "tag", - "font_class": "tag", - "unicode": "e668", - "unicode_decimal": 58984 - }, - { - "icon_id": "40519075", - "name": "card", - "font_class": "card", - "unicode": "e667", - "unicode_decimal": 58983 - }, - { - "icon_id": "40514741", - "name": "Steam Deck", - "font_class": "Steam", - "unicode": "e666", - "unicode_decimal": 58982 - }, - { - "icon_id": "40482759", - "name": "Android", - "font_class": "Android", - "unicode": "e661", - "unicode_decimal": 58977 - }, - { - "icon_id": "40482753", - "name": "Xbox", - "font_class": "Xbox", - "unicode": "e662", - "unicode_decimal": 58978 - }, - { - "icon_id": "40482751", - "name": "PS", - "font_class": "PS", - "unicode": "e663", - "unicode_decimal": 58979 - }, - { - "icon_id": "40482752", - "name": "Switch", - "font_class": "Switch", - "unicode": "e664", - "unicode_decimal": 58980 - }, - { - "icon_id": "40482750", - "name": "PC", - "font_class": "PC", - "unicode": "e665", - "unicode_decimal": 58981 - }, - { - "icon_id": "39973943", - "name": "thread", - "font_class": "thread", - "unicode": "e6ef", - "unicode_decimal": 59119 - }, - { - "icon_id": "39478612", - "name": "icon_post_notrecommend", - "font_class": "icon_post_Notrecommend", - "unicode": "e6ee", - "unicode_decimal": 59118 - }, - { - "icon_id": "38204830", - "name": "coupon-border", - "font_class": "coupon-border", - "unicode": "e660", - "unicode_decimal": 58976 - }, - { - "icon_id": "38160929", - "name": "date", - "font_class": "date", - "unicode": "e6ed", - "unicode_decimal": 59117 - }, - { - "icon_id": "37232886", - "name": "mic request border", - "font_class": "a-micrequestborder", - "unicode": "e65e", - "unicode_decimal": 58974 - }, - { - "icon_id": "37232885", - "name": "mic request", - "font_class": "a-micrequest", - "unicode": "e65f", - "unicode_decimal": 58975 - }, - { - "icon_id": "37060062", - "name": "icon-public-border", - "font_class": "public-border", - "unicode": "e65c", - "unicode_decimal": 58972 - }, - { - "icon_id": "37060061", - "name": "icon-private-border", - "font_class": "private-border", - "unicode": "e65d", - "unicode_decimal": 58973 - }, - { - "icon_id": "35589811", - "name": "female", - "font_class": "female", - "unicode": "e659", - "unicode_decimal": 58969 - }, - { - "icon_id": "35589812", - "name": "nonconforming", - "font_class": "nonconforming", - "unicode": "e65a", - "unicode_decimal": 58970 - }, - { - "icon_id": "35589813", - "name": "male", - "font_class": "male", - "unicode": "e65b", - "unicode_decimal": 58971 - }, - { - "icon_id": "34611227", - "name": "icon_exclamation", - "font_class": "icon_hint", - "unicode": "e6ec", - "unicode_decimal": 59116 - }, - { - "icon_id": "34303845", - "name": "stop", - "font_class": "stop", - "unicode": "e658", - "unicode_decimal": 58968 - }, - { - "icon_id": "34084643", - "name": "language", - "font_class": "language", - "unicode": "e654", - "unicode_decimal": 58964 - }, - { - "icon_id": "34084644", - "name": "account", - "font_class": "account", - "unicode": "e655", - "unicode_decimal": 58965 - }, - { - "icon_id": "34084645", - "name": "IM-setting", - "font_class": "IM-setting", - "unicode": "e656", - "unicode_decimal": 58966 - }, - { - "icon_id": "33241723", - "name": "info", - "font_class": "info", - "unicode": "e653", - "unicode_decimal": 58963 - }, - { - "icon_id": "33230167", - "name": "copy", - "font_class": "copy", - "unicode": "e6eb", - "unicode_decimal": 59115 - }, - { - "icon_id": "33070007", - "name": "icon-drag", - "font_class": "icon-drag", - "unicode": "e650", - "unicode_decimal": 58960 - }, - { - "icon_id": "33052223", - "name": "Invite to mic", - "font_class": "a-Invitetomic", - "unicode": "e6ea", - "unicode_decimal": 59114 - }, - { - "icon_id": "33043465", - "name": "voice-live", - "font_class": "voice-live", - "unicode": "e651", - "unicode_decimal": 58961 - }, - { - "icon_id": "32983364", - "name": "question", - "font_class": "question", - "unicode": "e64f", - "unicode_decimal": 58959 - }, - { - "icon_id": "32935983", - "name": "start-live", - "font_class": "a-createlive", - "unicode": "e64e", - "unicode_decimal": 58958 - }, - { - "icon_id": "32963380", - "name": "unfollow", - "font_class": "unfollow", - "unicode": "e6c8", - "unicode_decimal": 59080 - }, - { - "icon_id": "32963381", - "name": "following", - "font_class": "following", - "unicode": "e6d4", - "unicode_decimal": 59092 - }, - { - "icon_id": "32939490", - "name": "break link", - "font_class": "a-breaklink", - "unicode": "e6c5", - "unicode_decimal": 59077 - }, - { - "icon_id": "32931442", - "name": "icon-beauty-smooth", - "font_class": "icon-beauty-smooth", - "unicode": "e64a", - "unicode_decimal": 58954 - }, - { - "icon_id": "32931443", - "name": "icon-beauty-redness", - "font_class": "icon-beauty-redness", - "unicode": "e64b", - "unicode_decimal": 58955 - }, - { - "icon_id": "32931444", - "name": "icon-beauty-sharp", - "font_class": "icon-beauty-sharp", - "unicode": "e64c", - "unicode_decimal": 58956 - }, - { - "icon_id": "32931445", - "name": "icon-beauty-whitening", - "font_class": "icon-beauty-whitening", - "unicode": "e64d", - "unicode_decimal": 58957 - }, - { - "icon_id": "32926134", - "name": "volume down", - "font_class": "a-volumedown", - "unicode": "e647", - "unicode_decimal": 58951 - }, - { - "icon_id": "32926135", - "name": "volume mute", - "font_class": "a-volumemute", - "unicode": "e648", - "unicode_decimal": 58952 - }, - { - "icon_id": "32926136", - "name": "volume up", - "font_class": "a-volumeup", - "unicode": "e649", - "unicode_decimal": 58953 - }, - { - "icon_id": "32918681", - "name": "emoji-animated", - "font_class": "emoji-animated", - "unicode": "e6c4", - "unicode_decimal": 59076 - }, - { - "icon_id": "32867002", - "name": "flip", - "font_class": "flip", - "unicode": "e6e6", - "unicode_decimal": 59110 - }, - { - "icon_id": "32867003", - "name": "mic-border", - "font_class": "mic-border", - "unicode": "e6e7", - "unicode_decimal": 59111 - }, - { - "icon_id": "32867004", - "name": "random play", - "font_class": "a-randomplay", - "unicode": "e6e8", - "unicode_decimal": 59112 - }, - { - "icon_id": "32867079", - "name": "speaker-border", - "font_class": "speaker-border", - "unicode": "e6e9", - "unicode_decimal": 59113 - }, - { - "icon_id": "32866980", - "name": "backfullscreen", - "font_class": "backfullscreen", - "unicode": "e6ca", - "unicode_decimal": 59082 - }, - { - "icon_id": "32866981", - "name": "blocklisk", - "font_class": "blocklisk", - "unicode": "e6ce", - "unicode_decimal": 59086 - }, - { - "icon_id": "32866982", - "name": "beauty", - "font_class": "beauty", - "unicode": "e6d1", - "unicode_decimal": 59089 - }, - { - "icon_id": "32866983", - "name": "fullscreen-exit-fill", - "font_class": "fullscreen-exit-fill", - "unicode": "e6d3", - "unicode_decimal": 59091 - }, - { - "icon_id": "32866985", - "name": "fullscreen-fill", - "font_class": "fullscreen-fill", - "unicode": "e6d5", - "unicode_decimal": 59093 - }, - { - "icon_id": "32866986", - "name": "fans", - "font_class": "fans", - "unicode": "e6d6", - "unicode_decimal": 59094 - }, - { - "icon_id": "32866987", - "name": "fullscreen-mobile", - "font_class": "fullscreen-mobile", - "unicode": "e6d7", - "unicode_decimal": 59095 - }, - { - "icon_id": "32866988", - "name": "kick", - "font_class": "kick", - "unicode": "e6d8", - "unicode_decimal": 59096 - }, - { - "icon_id": "32866989", - "name": "list play", - "font_class": "a-listplay", - "unicode": "e6d9", - "unicode_decimal": 59097 - }, - { - "icon_id": "32866990", - "name": "previous", - "font_class": "previous", - "unicode": "e6da", - "unicode_decimal": 59098 - }, - { - "icon_id": "32866991", - "name": "data", - "font_class": "data", - "unicode": "e6db", - "unicode_decimal": 59099 - }, - { - "icon_id": "32866992", - "name": "gift effect", - "font_class": "a-gifteffect", - "unicode": "e6dc", - "unicode_decimal": 59100 - }, - { - "icon_id": "32866993", - "name": "gift-border", - "font_class": "gift-border", - "unicode": "e6dd", - "unicode_decimal": 59101 - }, - { - "icon_id": "32866994", - "name": "loop play", - "font_class": "a-loopplay", - "unicode": "e6de", - "unicode_decimal": 59102 - }, - { - "icon_id": "32866995", - "name": "notice", - "font_class": "notice", - "unicode": "e6df", - "unicode_decimal": 59103 - }, - { - "icon_id": "32866996", - "name": "time", - "font_class": "time", - "unicode": "e6e0", - "unicode_decimal": 59104 - }, - { - "icon_id": "32866997", - "name": "volume", - "font_class": "volume", - "unicode": "e6e1", - "unicode_decimal": 59105 - }, - { - "icon_id": "32866998", - "name": "playlist", - "font_class": "playlist", - "unicode": "e6e2", - "unicode_decimal": 59106 - }, - { - "icon_id": "32866999", - "name": "next", - "font_class": "next", - "unicode": "e6e3", - "unicode_decimal": 59107 - }, - { - "icon_id": "32867000", - "name": "music", - "font_class": "music", - "unicode": "e6e4", - "unicode_decimal": 59108 - }, - { - "icon_id": "32867001", - "name": "offMic", - "font_class": "offMic", - "unicode": "e6e5", - "unicode_decimal": 59109 - }, - { - "icon_id": "32865120", - "name": "pause", - "font_class": "pause", - "unicode": "e6c1", - "unicode_decimal": 59073 - }, - { - "icon_id": "32511005", - "name": "icon-fullImage", - "font_class": "icon-fullImage", - "unicode": "e646", - "unicode_decimal": 58950 - }, - { - "icon_id": "32447489", - "name": "icon-camera-fill", - "font_class": "icon-camera-fill", - "unicode": "e645", - "unicode_decimal": 58949 - }, - { - "icon_id": "32097519", - "name": "vote", - "font_class": "vote", - "unicode": "e6c0", - "unicode_decimal": 59072 - }, - { - "icon_id": "32102567", - "name": "icon-email", - "font_class": "icon-email", - "unicode": "e644", - "unicode_decimal": 58948 - }, - { - "icon_id": "17479540", - "name": "icon_chatroom_bigmenu", - "font_class": "icon_chatroom_bigmenu", - "unicode": "e674", - "unicode_decimal": 58996 - }, - { - "icon_id": "17479541", - "name": "icon_chatroom_smallmenu", - "font_class": "icon_chatroom_smallmenu", - "unicode": "e675", - "unicode_decimal": 58997 - }, - { - "icon_id": "18249546", - "name": "chatroom_mic", - "font_class": "icon_chatroom_mic", - "unicode": "e685", - "unicode_decimal": 59013 - }, - { - "icon_id": "18249552", - "name": "chatroom_on", - "font_class": "icon_on", - "unicode": "e687", - "unicode_decimal": 59015 - }, - { - "icon_id": "18249568", - "name": "exit", - "font_class": "icon_exit", - "unicode": "e688", - "unicode_decimal": 59016 - }, - { - "icon_id": "18294099", - "name": "chatroom_more", - "font_class": "icon_chatroom_more", - "unicode": "e68f", - "unicode_decimal": 59023 - }, - { - "icon_id": "18594409", - "name": "icon_fobidden", - "font_class": "jinmai", - "unicode": "e694", - "unicode_decimal": 59028 - }, - { - "icon_id": "23067626", - "name": "icon_share invtation", - "font_class": "a-icon_shareinvtation", - "unicode": "e6ab", - "unicode_decimal": 59051 - }, - { - "icon_id": "23771600", - "name": "ICON_entrance", - "font_class": "icon_entrance", - "unicode": "e6bc", - "unicode_decimal": 59068 - }, - { - "icon_id": "13968079", - "name": "icon_comunity_delete", - "font_class": "icon_comunity_delete", - "unicode": "e643", - "unicode_decimal": 58947 - }, - { - "icon_id": "13968080", - "name": "icon_comunity_reply", - "font_class": "icon_comunity_reply", - "unicode": "e63e", - "unicode_decimal": 58942 - }, - { - "icon_id": "13968081", - "name": "icon_community_like", - "font_class": "icon_community_like", - "unicode": "e63f", - "unicode_decimal": 58943 - }, - { - "icon_id": "13968082", - "name": "icon_comunity_report", - "font_class": "icon_comunity_report", - "unicode": "e640", - "unicode_decimal": 58944 - }, - { - "icon_id": "13968083", - "name": "icon_comunity_topics", - "font_class": "icon_comunity_topics", - "unicode": "e641", - "unicode_decimal": 58945 - }, - { - "icon_id": "13968084", - "name": "icon_comunity_comment", - "font_class": "icon_comunity_comment", - "unicode": "e642", - "unicode_decimal": 58946 - }, - { - "icon_id": "20241151", - "name": "icon_menu_Customer", - "font_class": "icon_menu_Customer", - "unicode": "e6a4", - "unicode_decimal": 59044 - }, - { - "icon_id": "26233594", - "name": "icon_post_collection", - "font_class": "icon_post_collection", - "unicode": "e6c2", - "unicode_decimal": 59074 - }, - { - "icon_id": "26233595", - "name": "icon_post_component", - "font_class": "icon_post_component", - "unicode": "e6c3", - "unicode_decimal": 59075 - }, - { - "icon_id": "26233598", - "name": "icon_post_recommend", - "font_class": "icon_post_recommend", - "unicode": "e6c6", - "unicode_decimal": 59078 - }, - { - "icon_id": "26233599", - "name": "icon_post_highlight", - "font_class": "icon_post_highlight", - "unicode": "e6c7", - "unicode_decimal": 59079 - }, - { - "icon_id": "26363855", - "name": "icon_post_viewers", - "font_class": "icon_post_viewers", - "unicode": "e6c9", - "unicode_decimal": 59081 - }, - { - "icon_id": "27543804", - "name": "icon_post_chitchat", - "font_class": "icon_post_chitchat", - "unicode": "e6cc", - "unicode_decimal": 59084 - }, - { - "icon_id": "27543805", - "name": "icon_post_selfies", - "font_class": "icon_post_selfies", - "unicode": "e6cd", - "unicode_decimal": 59085 - }, - { - "icon_id": "27983298", - "name": "icon_order_tip", - "font_class": "icon_order_tip", - "unicode": "e6d0", - "unicode_decimal": 59088 - }, - { - "icon_id": "30038315", - "name": "icon_order_remark", - "font_class": "icon_order_remark", - "unicode": "e6d2", - "unicode_decimal": 59090 - }, - { - "icon_id": "12908387", - "name": "icon_refund", - "font_class": "icon_refund", - "unicode": "e63c", - "unicode_decimal": 58940 - }, - { - "icon_id": "32073072", - "name": "icon-post", - "font_class": "icon-post", - "unicode": "e63b", - "unicode_decimal": 58939 - }, - { - "icon_id": "14348065", - "name": "backtop", - "font_class": "backtop", - "unicode": "e63d", - "unicode_decimal": 58941 - }, - { - "icon_id": "32058809", - "name": "faq", - "font_class": "faq", - "unicode": "e6ba", - "unicode_decimal": 59066 - }, - { - "icon_id": "32058810", - "name": "onlineservice", - "font_class": "onlineservice", - "unicode": "e6bb", - "unicode_decimal": 59067 - }, - { - "icon_id": "32020329", - "name": "icon-vip", - "font_class": "icon-vip", - "unicode": "e638", - "unicode_decimal": 58936 - }, - { - "icon_id": "32020330", - "name": "icon-legends", - "font_class": "icon-legends", - "unicode": "e639", - "unicode_decimal": 58937 - }, - { - "icon_id": "32020331", - "name": "icon-influencer", - "font_class": "icon-influencer", - "unicode": "e63a", - "unicode_decimal": 58938 - }, - { - "icon_id": "32017893", - "name": "icon-comment", - "font_class": "icon-comment", - "unicode": "e636", - "unicode_decimal": 58934 - }, - { - "icon_id": "32017819", - "name": "icon-helpCenter", - "font_class": "icon-helpCenter", - "unicode": "e634", - "unicode_decimal": 58932 - }, - { - "icon_id": "32017822", - "name": "icon-setting", - "font_class": "icon-setting", - "unicode": "e635", - "unicode_decimal": 58933 - }, - { - "icon_id": "32017700", - "name": "icon-reload", - "font_class": "icon-reload", - "unicode": "e633", - "unicode_decimal": 58931 - }, - { - "icon_id": "32006043", - "name": "icon-reset", - "font_class": "icon-reset", - "unicode": "e631", - "unicode_decimal": 58929 - }, - { - "icon_id": "31957204", - "name": "icon-addCard", - "font_class": "icon-addCard", - "unicode": "e62e", - "unicode_decimal": 58926 - }, - { - "icon_id": "31957205", - "name": "icon-send", - "font_class": "icon-send", - "unicode": "e630", - "unicode_decimal": 58928 - }, - { - "icon_id": "31957611", - "name": "icon-service-fill", - "font_class": "icon-service-fill", - "unicode": "e632", - "unicode_decimal": 58930 - }, - { - "icon_id": "31956004", - "name": "icon-service", - "font_class": "icon-service", - "unicode": "e62f", - "unicode_decimal": 58927 - }, - { - "icon_id": "31946118", - "name": "icon-random", - "font_class": "icon-random", - "unicode": "e62d", - "unicode_decimal": 58925 - }, - { - "icon_id": "31937875", - "name": "icon-keyboard", - "font_class": "icon-keyboard", - "unicode": "e62c", - "unicode_decimal": 58924 - }, - { - "icon_id": "31937869", - "name": "icon-people", - "font_class": "icon-people", - "unicode": "e629", - "unicode_decimal": 58921 - }, - { - "icon_id": "31937870", - "name": "icon_top", - "font_class": "icon_top", - "unicode": "e62b", - "unicode_decimal": 58923 - }, - { - "icon_id": "31937078", - "name": "icon-order-fill", - "font_class": "icon-order-fill", - "unicode": "e62a", - "unicode_decimal": 58922 - }, - { - "icon_id": "31933275", - "name": "add", - "font_class": "add", - "unicode": "e6b5", - "unicode_decimal": 59061 - }, - { - "icon_id": "31933276", - "name": "reduce", - "font_class": "reduce", - "unicode": "e6b6", - "unicode_decimal": 59062 - }, - { - "icon_id": "31916329", - "name": "icon-contact", - "font_class": "icon-contact", - "unicode": "e616", - "unicode_decimal": 58902 - }, - { - "icon_id": "31915902", - "name": "Warning", - "font_class": "Warning", - "unicode": "e614", - "unicode_decimal": 58900 - }, - { - "icon_id": "25786453", - "name": "icon_im_purpleface", - "font_class": "icon_im_purpleface", - "unicode": "e6bd", - "unicode_decimal": 59069 - }, - { - "icon_id": "25786454", - "name": "icon_im_emoji", - "font_class": "icon_im_emoji", - "unicode": "e6bf", - "unicode_decimal": 59071 - }, - { - "icon_id": "27686233", - "name": "icon_im_other", - "font_class": "icon_im_other", - "unicode": "e6cf", - "unicode_decimal": 59087 - }, - { - "icon_id": "18249550", - "name": "chatroom_off", - "font_class": "msgmute", - "unicode": "e686", - "unicode_decimal": 59014 - }, - { - "icon_id": "24535102", - "name": "icon_IM_blocked", - "font_class": "block", - "unicode": "e6b9", - "unicode_decimal": 59065 - }, - { - "icon_id": "18255393", - "name": "icon_call_mute", - "font_class": "mute", - "unicode": "e684", - "unicode_decimal": 59012 - }, - { - "icon_id": "23525940", - "name": "icon_call_openmic", - "font_class": "open-mic", - "unicode": "e6b4", - "unicode_decimal": 59060 - }, - { - "icon_id": "18255394", - "name": "icon_call_hangup", - "font_class": "hang-up", - "unicode": "e68d", - "unicode_decimal": 59021 - }, - { - "icon_id": "18529424", - "name": "icon_interact", - "font_class": "emoji", - "unicode": "e693", - "unicode_decimal": 59027 - }, - { - "icon_id": "31882851", - "name": "quote", - "font_class": "quote", - "unicode": "e6b3", - "unicode_decimal": 59059 - }, - { - "icon_id": "31882806", - "name": "checked", - "font_class": "checked", - "unicode": "e6b2", - "unicode_decimal": 59058 - }, - { - "icon_id": "31882794", - "name": "report", - "font_class": "report", - "unicode": "e6b1", - "unicode_decimal": 59057 - }, - { - "icon_id": "12908372", - "name": "icon_delete", - "font_class": "trashcan", - "unicode": "e612", - "unicode_decimal": 58898 - }, - { - "icon_id": "19500607", - "name": "icon_public", - "font_class": "public", - "unicode": "e698", - "unicode_decimal": 59032 - }, - { - "icon_id": "19500711", - "name": "icon_private", - "font_class": "private", - "unicode": "e699", - "unicode_decimal": 59033 - }, - { - "icon_id": "19675398", - "name": "icon_uploadimg", - "font_class": "uploadimg", - "unicode": "e69c", - "unicode_decimal": 59036 - }, - { - "icon_id": "19675399", - "name": "icon_uploadclip", - "font_class": "uploadclip", - "unicode": "e69d", - "unicode_decimal": 59037 - }, - { - "icon_id": "31882415", - "name": "arrow-down-border", - "font_class": "arrow-down-border", - "unicode": "e6af", - "unicode_decimal": 59055 - }, - { - "icon_id": "31882430", - "name": "arrow-left", - "font_class": "arrow-left", - "unicode": "e6b0", - "unicode_decimal": 59056 - }, - { - "icon_id": "31882412", - "name": "eye-off", - "font_class": "eye-off", - "unicode": "e6ae", - "unicode_decimal": 59054 - }, - { - "icon_id": "31882406", - "name": "eye-on", - "font_class": "eye-on", - "unicode": "e6ad", - "unicode_decimal": 59053 - }, - { - "icon_id": "31818154", - "name": "arrow-down-fill", - "font_class": "arrow-down-fill", - "unicode": "e6ac", - "unicode_decimal": 59052 - }, - { - "icon_id": "31818069", - "name": "arrow-left-border", - "font_class": "arrow-left-border", - "unicode": "e6aa", - "unicode_decimal": 59050 - }, - { - "icon_id": "31801946", - "name": "delete-02", - "font_class": "delete-02", - "unicode": "e628", - "unicode_decimal": 58920 - }, - { - "icon_id": "31801045", - "name": "arrow-down-circle", - "font_class": "arrow-down-circle", - "unicode": "e601", - "unicode_decimal": 58881 - }, - { - "icon_id": "31801046", - "name": "arrow-right-border", - "font_class": "arrow-right-border", - "unicode": "e602", - "unicode_decimal": 58882 - }, - { - "icon_id": "31801047", - "name": "arrow-up-fill", - "font_class": "arrow-up-fill", - "unicode": "e603", - "unicode_decimal": 58883 - }, - { - "icon_id": "31801048", - "name": "arrow-right", - "font_class": "arrow-right", - "unicode": "e604", - "unicode_decimal": 58884 - }, - { - "icon_id": "31801049", - "name": "Chat", - "font_class": "Chat", - "unicode": "e605", - "unicode_decimal": 58885 - }, - { - "icon_id": "31801051", - "name": "delete", - "font_class": "close", - "unicode": "e607", - "unicode_decimal": 58887 - }, - { - "icon_id": "31801052", - "name": "Create room", - "font_class": "a-Createroom", - "unicode": "e608", - "unicode_decimal": 58888 - }, - { - "icon_id": "31801053", - "name": "Call", - "font_class": "Call", - "unicode": "e609", - "unicode_decimal": 58889 - }, - { - "icon_id": "31801054", - "name": "Like-fill", - "font_class": "Like-fill", - "unicode": "e60a", - "unicode_decimal": 58890 - }, - { - "icon_id": "31801055", - "name": "filter-fill", - "font_class": "filter-fill", - "unicode": "e60b", - "unicode_decimal": 58891 - }, - { - "icon_id": "31801056", - "name": "live", - "font_class": "live", - "unicode": "e60c", - "unicode_decimal": 58892 - }, - { - "icon_id": "31801057", - "name": "follow", - "font_class": "follow", - "unicode": "e60d", - "unicode_decimal": 58893 - }, - { - "icon_id": "31801058", - "name": "Play", - "font_class": "Play", - "unicode": "e60e", - "unicode_decimal": 58894 - }, - { - "icon_id": "31801059", - "name": "events", - "font_class": "events", - "unicode": "e60f", - "unicode_decimal": 58895 - }, - { - "icon_id": "31801060", - "name": "More", - "font_class": "More", - "unicode": "e610", - "unicode_decimal": 58896 - }, - { - "icon_id": "31801061", - "name": "matching", - "font_class": "matching", - "unicode": "e611", - "unicode_decimal": 58897 - }, - { - "icon_id": "31801063", - "name": "Like", - "font_class": "Like", - "unicode": "e613", - "unicode_decimal": 58899 - }, - { - "icon_id": "31801065", - "name": "loading", - "font_class": "loading", - "unicode": "e615", - "unicode_decimal": 58901 - }, - { - "icon_id": "31801067", - "name": "Search", - "font_class": "Search", - "unicode": "e617", - "unicode_decimal": 58903 - }, - { - "icon_id": "31801068", - "name": "filter", - "font_class": "filter", - "unicode": "e618", - "unicode_decimal": 58904 - }, - { - "icon_id": "31801069", - "name": "social-facebook", - "font_class": "social-facebook", - "unicode": "e619", - "unicode_decimal": 58905 - }, - { - "icon_id": "31801070", - "name": "select", - "font_class": "select", - "unicode": "e61a", - "unicode_decimal": 58906 - }, - { - "icon_id": "31801071", - "name": "Share-border", - "font_class": "Share-border", - "unicode": "e61b", - "unicode_decimal": 58907 - }, - { - "icon_id": "31801072", - "name": "social-instagram", - "font_class": "social-instagram", - "unicode": "e61c", - "unicode_decimal": 58908 - }, - { - "icon_id": "31801073", - "name": "social-twitch", - "font_class": "social-twitch", - "unicode": "e61d", - "unicode_decimal": 58909 - }, - { - "icon_id": "31801074", - "name": "Voice", - "font_class": "Voice", - "unicode": "e61e", - "unicode_decimal": 58910 - }, - { - "icon_id": "31801075", - "name": "social-tiktok", - "font_class": "social-tiktok", - "unicode": "e61f", - "unicode_decimal": 58911 - }, - { - "icon_id": "31801076", - "name": "social-google", - "font_class": "social-google", - "unicode": "e620", - "unicode_decimal": 58912 - }, - { - "icon_id": "31801077", - "name": "social-youtube", - "font_class": "social-youtube", - "unicode": "e621", - "unicode_decimal": 58913 - }, - { - "icon_id": "31801078", - "name": "social-apple", - "font_class": "social-apple", - "unicode": "e622", - "unicode_decimal": 58914 - }, - { - "icon_id": "31801079", - "name": "icon-kfckfc", - "font_class": "social-twitter", - "unicode": "e623", - "unicode_decimal": 58915 - }, - { - "icon_id": "31801080", - "name": "star-fill", - "font_class": "star-fill", - "unicode": "e624", - "unicode_decimal": 58916 - }, - { - "icon_id": "31801081", - "name": "star", - "font_class": "star", - "unicode": "e625", - "unicode_decimal": 58917 - }, - { - "icon_id": "31801082", - "name": "social-discord", - "font_class": "social-discord", - "unicode": "e626", - "unicode_decimal": 58918 - }, - { - "icon_id": "31801083", - "name": "messages", - "font_class": "messages", - "unicode": "e627", - "unicode_decimal": 58919 - } - ] -} diff --git a/crush/Crush/Src/Components/IconFont/iconfont.ttf b/crush/Crush/Src/Components/IconFont/iconfont.ttf deleted file mode 100644 index 4253f73..0000000 Binary files a/crush/Crush/Src/Components/IconFont/iconfont.ttf and /dev/null differ diff --git a/crush/Crush/Src/Components/Navigate/NavigatorMap.swift b/crush/Crush/Src/Components/Navigate/NavigatorMap.swift deleted file mode 100644 index b950396..0000000 --- a/crush/Crush/Src/Components/Navigate/NavigatorMap.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// URLNavigator.swift -// Crush -// -// Created by Leon on 2025/7/13. -// - -import Foundation -import UIKit -import URLMatcher -import URLNavigator - -enum NavigationMap { - static func initialize(navigator: NavigatorProtocol) { - navigator.register("http://", webViewControllerFactory) - navigator.register("https://", webViewControllerFactory) - - navigator.handle("\(AppConst.schemePrefix)alert", alert(navigator: navigator)) - - navigator.handle("\(AppConst.schemePrefix)aichat/") { url, values, _ -> Bool in - dlog("[Navigator] NavigationMap.\(url) \(#function):\(#line) - global fallback function is called") - guard let userIdStr = values["aiId"] as? String, let userId = Int(userIdStr) else { - return false - } - -// let tmpUrl = url.urlStringValue.replacingOccurrences(of: AppConst.schemePrefix, with: "https://") -// if let url = URL(string: tmpUrl), let components = URLComponents(url: url, resolvingAgainstBaseURL: true) { -// if let queryItems = components.queryItems { -// for item in queryItems { -// // print("\(item.name): \(String(describing: item.value))") -// if item.name == "productId", let idStr = item.value, let id = Int(idStr) { -// // .. -// } -// } -// } -// } - if UserCore.shared.isLogin(){ - AppRouter.goChatVC(aiId: userId) - }else{ - AppRouter.goAIRoleHome(aiId: userId) - } - - // AppRouter.goPersonalPageController(userId: userId, productId: productId) - return true - } - - navigator.handle("\(AppConst.schemePrefix)route/") { (url, values: [String: Any], _) -> Bool in - dlog("[Navigator] NavigationMap.\(url) \(#function):\(#line) - global fallback function is called") - guard let uri = values["uri"] as? String else { - return false - } - if uri == "report" { - // ... - } - - return true - } - } - - // MARK: - helper - - func convertQueryParams(urlstr: URLConvertible) -> [URLQueryItem] { - guard let urlPath = urlstr.urlStringValue as? String else { - return [] - } - // var dict: Dictionary? - var allkeysValues = [String: Any]() - if let url = URL(string: urlPath), let components = URLComponents(url: url, resolvingAgainstBaseURL: true) { - if let queryItems = components.queryItems { - return queryItems - } - } - - return [] - } - - // MARK: - factory - - private static func webViewControllerFactory( - url: URLConvertible, - values: [String: Any], - context: Any? - ) -> UIViewController? { - guard let url = url.urlValue else { return nil } - let h5 = H5Controller() - h5.loadURL(url: url) - return h5 - } - - // MARK: - other - - private static func alert(navigator: NavigatorProtocol) -> URLOpenHandlerFactory { - return { url, _, _ in - guard let title = url.queryParameters["title"] else { return false } - let message = url.queryParameters["message"] - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) - navigator.present(alertController, wrap: nil, from: nil, animated: true, completion: nil) - return true - } - } -} diff --git a/crush/Crush/Src/Components/Photo/AvatarCrop/CIrcleCropView.h b/crush/Crush/Src/Components/Photo/AvatarCrop/CIrcleCropView.h deleted file mode 100644 index 97189d4..0000000 --- a/crush/Crush/Src/Components/Photo/AvatarCrop/CIrcleCropView.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// CIrcleCropView.h -// CIrcleCropView -// -// Created by Bhavesh Chaudhari on 07/05/20. -// Copyright © 2020 Bhavesh. All rights reserved. -// - -#import - -//! Project version number for CIrcleCropView. -FOUNDATION_EXPORT double CIrcleCropViewVersionNumber; - -//! Project version string for CIrcleCropView. -FOUNDATION_EXPORT const unsigned char CIrcleCropViewVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/crush/Crush/Src/Components/Photo/AvatarCrop/CircleCropView.swift b/crush/Crush/Src/Components/Photo/AvatarCrop/CircleCropView.swift deleted file mode 100644 index d405cef..0000000 --- a/crush/Crush/Src/Components/Photo/AvatarCrop/CircleCropView.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// CircleCropView.swift -// CIrcleCropView Test -// -// Created by Bhavesh Chaudhari on 08/05/20. -// Copyright © 2020 Bhavesh. All rights reserved. -// - -import UIKit - - -public class CircleCropView: UIView { - - - override init(frame: CGRect) { - super.init(frame: frame) - self.backgroundColor = UIColor.black.withAlphaComponent(0.58) - isUserInteractionEnabled = false - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - var circleInset: CGRect { - let rect = bounds - let minSize = min(rect.width, rect.height) - let hole = CGRect(x: (rect.width - minSize) / 2, y: (rect.height - minSize) / 2, width: minSize, height: minSize).insetBy(dx: 15, dy: 15) - return hole - } - - override public func draw(_ rect: CGRect) { -// guard let context = UIGraphicsGetCurrentContext() else { return } -// context.saveGState() -// let holeInset = circleInset -// context.addRect(holeInset) -// context.clip() -// context.clear(holeInset) -// -// context.draw(UIImage(named: "WhiteGrid.png")!.cgImage!, in: holeInset) -// context.setFillColor(UIColor.clear.cgColor) -// context.fill( holeInset) -// context.setStrokeColor(UIColor.white.cgColor) -// context.strokeEllipse(in: holeInset) -// context.restoreGState() - - guard let context = UIGraphicsGetCurrentContext() else { return } - context.saveGState() - let holeInset = circleInset - context.addRect(holeInset) - context.clip() - context.clear(holeInset) - - context.draw(UIImage(named: "WhiteGrid.png")!.cgImage!, in: holeInset) - context.setFillColor(UIColor.clear.cgColor) - context.fill( holeInset) - context.setStrokeColor(UIColor.white.cgColor) - context.strokeEllipse(in: holeInset) - context.restoreGState() - - } -} diff --git a/crush/Crush/Src/Components/Photo/AvatarCrop/CircleCropViewController.swift b/crush/Crush/Src/Components/Photo/AvatarCrop/CircleCropViewController.swift deleted file mode 100644 index 542f1dd..0000000 --- a/crush/Crush/Src/Components/Photo/AvatarCrop/CircleCropViewController.swift +++ /dev/null @@ -1,178 +0,0 @@ -// -// CircleCropViewController.swift -// CIrcleCropView -// -// Created by Bhavesh Chaudhari on 08/05/20. -// Copyright © 2020 Bhavesh. All rights reserved. -// - -import UIKit - -public class CropViewController: UIViewController { - var image: UIImage - let imageView: UIImageView - let scrollView: UIScrollView - let completion: (UIImage?) -> Void - private var circleView: CircleCropView? - - var backButton: StyleButton = { - let button = StyleButton() - button.tertiary(size: .large) - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle("Cancel", for: .normal) - return button - }() - - var okButton: StyleButton = { - let button = StyleButton() - button.primary(size: .large) - button.setTitle("Confirm", for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - public init(image: UIImage, completion: @escaping (UIImage?) -> Void) { - self.image = image - self.completion = completion - imageView = UIImageView(image: image) - scrollView = UIScrollView() - scrollView.contentInsetAdjustmentBehavior = .never - super.init(nibName: nil, bundle: nil) - } - - override public func viewDidLoad() { - super.viewDidLoad() - circleView = CircleCropView(frame: view.bounds) - view.addSubview(scrollView) - view.addSubview(circleView!) - view.addSubview(okButton) - view.addSubview(backButton) - scrollView.addSubview(imageView) - scrollView.contentSize = image.size - scrollView.delegate = self - view.backgroundColor = UIColor(red: 33/255.0, green: 26/255.0, blue: 43/255.0, alpha: 1) - scrollView.frame = view.frame//.inset(by: view.safeAreaInsets) - circleView?.frame = scrollView.frame//.inset(by: view.safeAreaInsets) - - backButton.addTarget(self, action: #selector(backClick), for: .touchUpInside) - okButton.addTarget(self, action: #selector(okClick), for: .touchUpInside) - - modalPresentationStyle = .fullScreen - - addConstraint() - } - - func addConstraint() { - backButton.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) - } - okButton.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) - } - } - - override public var preferredStatusBarStyle: UIStatusBarStyle { - return .default - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(true, animated: animated) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - let scrollFrame = scrollView.frame - let imSize = image.size - guard let hole = circleView?.circleInset, hole.width > 0 else { return } - let verticalRatio = hole.height / imSize.height - let horizontalRatio = hole.width / imSize.width - let maxRatio = max(horizontalRatio, verticalRatio) - if(maxRatio > 1){ // 图比裁剪区域小 - scrollView.minimumZoomScale = maxRatio - scrollView.maximumZoomScale = maxRatio * 2 - scrollView.zoomScale = maxRatio - }else{ - scrollView.minimumZoomScale = maxRatio - scrollView.maximumZoomScale = 1 - scrollView.zoomScale = scrollView.minimumZoomScale - } - - - let insetHeight = (scrollFrame.height - hole.height) / 2 - let insetWidth = (scrollFrame.width - hole.width) / 2 - scrollView.contentInset = UIEdgeInsets(top: insetHeight, left: insetWidth, bottom: insetHeight, right: insetWidth) - okButton.clipsToBounds = true - } - - override public func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - navigationController?.setNavigationBarHidden(false, animated: animated) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc func backClick(sender: UIButton) { - dismiss(animated: true, completion: nil) - } - - @objc func okClick(sender: UIButton) { - cropImage() - } - - private func cropImage() { - guard let rect = circleView?.circleInset else { return } - let shift = rect.applying(CGAffineTransform(translationX: scrollView.contentOffset.x, y: scrollView.contentOffset.y)) - let scaled = shift.applying(CGAffineTransform(scaleX: 1.0 / scrollView.zoomScale, y: 1.0 / scrollView.zoomScale)) - let newImage = image.imageCropped(toRect: scaled) - completion(newImage) - dismiss(animated: true, completion: nil) - } -} - -extension CropViewController: UIScrollViewDelegate { - func zoomOut() { - let newScale = scrollView.zoomScale == scrollView.minimumZoomScale ? 0.5 : scrollView.minimumZoomScale - scrollView.setZoomScale(newScale, animated: true) - } - - public func viewForZooming(in scrollView: UIScrollView) -> UIView? { - return imageView - } - - public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { - // need empty implementation for zooming - } -} - -extension UIImage { - func imageCropped(toRect rect: CGRect) -> UIImage { - let rad: (Double) -> CGFloat = { deg in - CGFloat(deg / 180.0 * .pi) - } - var rectTransform: CGAffineTransform - switch imageOrientation { - case .left: - let rotation = CGAffineTransform(rotationAngle: rad(90)) - rectTransform = rotation.translatedBy(x: 0, y: -size.height) - case .right: - let rotation = CGAffineTransform(rotationAngle: rad(-90)) - rectTransform = rotation.translatedBy(x: -size.width, y: 0) - case .down: - let rotation = CGAffineTransform(rotationAngle: rad(-180)) - rectTransform = rotation.translatedBy(x: -size.width, y: -size.height) - default: - rectTransform = .identity - } - rectTransform = rectTransform.scaledBy(x: scale, y: scale) - let transformedRect = rect.applying(rectTransform) - let imageRef = cgImage!.cropping(to: transformedRect)! - let result = UIImage(cgImage: imageRef, scale: scale, orientation: imageOrientation) - print("croped Image width and height = \(result.size)") - return result - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/BrowseImageZoomView.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/BrowseImageZoomView.swift deleted file mode 100644 index 14e58e2..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/BrowseImageZoomView.swift +++ /dev/null @@ -1,409 +0,0 @@ -// -// BrowseImageZoomView.swift -// Crush -// -// Created by Leon on 2025/7/26. -// - -import SnapKit -import UIKit - -protocol BrowseImageZoomViewDelegate: AnyObject { - func dismisAnimation(_ zoomImageView: UIImageView, toFrame: CGRect) - func singleTapZoomView(_ zoomImageView: UIImageView) -> Bool - func longPressZoomView(_ zoomView: BrowseImageZoomView) -> Bool - func setVCViewBackgroundColor(_ alpha: CGFloat) -} - -class BrowseImageZoomView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { - private let kMaxScale: CGFloat = 3.0 - private let kMaxDropHeight: CGFloat = 60 - - private var imageUrl: String? - private var progress: PhotoBrowseProgressView? - private var noticeButton: UIButton? - private var stateLabel: UILabel? - private var origFrame: CGRect = .zero - - var scrollView: UIScrollView! - var imageView: UIImageView! - weak var delegate: BrowseImageZoomViewDelegate? - var displayIndex: Int = 0 - var imageModel: PhotoBrowserModel? - - init(imageModel: PhotoBrowserModel) { - super.init(frame: .zero) - self.imageModel = imageModel - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .clear - - scrollView = UIScrollView() - scrollView.backgroundColor = UIColor.black.withAlphaComponent(1.0) // Assuming kBackgroundColor(1) is black - scrollView.clipsToBounds = true - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.delegate = self - if #available(iOS 11.0, *) { - scrollView.contentInsetAdjustmentBehavior = .never - } - addSubview(scrollView) - - // Using SnapKit for scrollView layout - scrollView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - imageView = UIImageView(frame: CGRect( - x: 0, - y: (UIScreen.main.bounds.height - UIScreen.main.bounds.width) / 2, - width: UIScreen.main.bounds.width, - height: UIScreen.main.bounds.width - )) - imageView.backgroundColor = .clear - imageView.image = imageModel?.placeHolder - scrollView.addSubview(imageView) - - addGestures() - reloadImage(imageModel) - } - - private func addGestures() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(singleGestureAction(_:))) - addGestureRecognizer(tapGesture) - - let doubleGesture = UITapGestureRecognizer(target: self, action: #selector(doubleGestureAction(_:))) - doubleGesture.numberOfTapsRequired = 2 - addGestureRecognizer(doubleGesture) - tapGesture.require(toFail: doubleGesture) - - let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureAction(_:))) - addGestureRecognizer(longPressGesture) - - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:))) - panGesture.delegate = self - addGestureRecognizer(panGesture) - } - - private func resetErrorNoticeView() { - stateLabel?.removeFromSuperview() - stateLabel = nil - noticeButton?.removeFromSuperview() - noticeButton = nil - } - - func reloadImage(_ imageModel: PhotoBrowserModel?) { - guard let imageModel = imageModel else { return } - self.imageModel = imageModel - - if let image = imageModel.image { - imageView.image = image - updateImageViewFrame() - } - if let url = imageModel.imageUrl { - loadImageWithImageUrl(url, animated: imageModel.sourceRect.size.width > 0) - } - } - - private func loadImageWithImageUrl(_ imageUrl: String, animated:Bool = true) { - self.imageUrl = imageUrl -// guard let url = URL(string: imageUrl) else { return } -// if let cacheImage = SDImageCache.shared.imageFromDiskCache(forKey: url.absoluteString) { -// imageView.image = cacheImage -// resetErrorNoticeView() -// setNeedsLayout() -// imageView.setNeedsDisplay() -// updateImageViewFrame() -// return -// } - - progress = PhotoBrowseProgressView() - progress?.isHidden = false - addSubview(progress!) - progress?.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 50, height: 50)) - make.center.equalToSuperview() - } - - imageView.loadImage(imageUrl) { receivedSize, totalSize in - guard self.imageUrl == imageUrl, totalSize > 0 else { return } // let self = self, - DispatchQueue.main.async { - self.progress?.progress = CGFloat(receivedSize) / CGFloat(totalSize) - } - } completionBlock: { result in - guard self.imageUrl == imageUrl else { return } // let self = self, - - DispatchQueue.main.async { - self.progress?.removeFromSuperview() - self.progress = nil - - switch result { - case let .success(imageResult): - let duration = animated ? 0.25 : 0 - UIView.animate(withDuration: duration) { - self.imageView.image = imageResult.image - self.resetErrorNoticeView() - self.setNeedsLayout() - self.imageView.setNeedsDisplay() - self.updateImageViewFrame() - } - case let .failure(error): - dlog("图片加载失败: \(error.localizedDescription)") - let button = UIButton() - button.isUserInteractionEnabled = false - button.setImage(UIImage(named: "icon_notice_white"), for: .normal) - self.addSubview(button) - self.noticeButton = button - button.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 16, height: 16)) - make.centerX.equalToSuperview() - make.centerY.equalToSuperview().offset(-36) - } - - let errorLabel = UILabel() - errorLabel.backgroundColor = .clear - errorLabel.textColor = .white - errorLabel.textAlignment = .center - errorLabel.clipsToBounds = true - errorLabel.text = NSLocalizedString("image_load_failed", comment: "") - errorLabel.font = .systemFont(ofSize: 14) - errorLabel.numberOfLines = 2 - errorLabel.sizeToFit() - self.addSubview(errorLabel) - errorLabel.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: UIScreen.main.bounds.width - 80, height: 30)) - make.center.equalToSuperview() - } - self.stateLabel = errorLabel - } - } - } - /* - imageView.sd_setImage(with: url, placeholderImage: imageModel?.placeHolder, options: [.retryFailed, .lowPriority, .handleCookies]) { [weak self] receivedSize, expectedSize, _ in - guard let self = self, self.imageUrl == imageUrl, expectedSize > 0 else { return } - DispatchQueue.main.async { - self.progress?.progress = CGFloat(receivedSize) / CGFloat(expectedSize) - } - } completed: { [weak self] image, error, _, _ in - guard let self = self, self.imageUrl == imageUrl else { return } - self.progress?.removeFromSuperview() - self.progress = nil - - if let error = error { - let button = UIButton() - button.isUserInteractionEnabled = false - button.setImage(UIImage(named: "icon_notice_white"), for: .normal) - self.addSubview(button) - self.noticeButton = button - button.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 16, height: 16)) - make.centerX.equalToSuperview() - make.centerY.equalToSuperview().offset(-36) - } - - let errorLabel = UILabel() - errorLabel.backgroundColor = .clear - errorLabel.textColor = .white - errorLabel.textAlignment = .center - errorLabel.clipsToBounds = true - errorLabel.text = NSLocalizedString("image_load_failed", comment: "") - errorLabel.font = .systemFont(ofSize: 14) - errorLabel.numberOfLines = 2 - errorLabel.sizeToFit() - errorLabel.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: UIScreen.main.bounds.width - 80, height: 30)) - make.center.equalToSuperview() - } - self.addSubview(errorLabel) - self.stateLabel = errorLabel - } else { - UIView.animate(withDuration: 0.25) { - self.imageView.image = image - self.resetErrorNoticeView() - self.setNeedsLayout() - self.imageView.setNeedsDisplay() - self.updateImageViewFrame() - } - } - }*/ - } - - private func updateImageViewFrame() { - guard let image = imageView.image, image.size.height > 0, image.size.width > 0 else { return } - - let screenHeight = UIScreen.main.bounds.height - let imageRatio = image.size.width / image.size.height - let newWidth = frame.width - let newHeight = frame.width / imageRatio - let newX: CGFloat = 0 - let newY = newHeight > screenHeight ? 0 : (screenHeight - newHeight) / 2 - - let newRect = CGRect(x: newX, y: newY, width: newWidth, height: newHeight) - imageView.frame = newRect - origFrame = newRect - - scrollView.contentSize = CGSize(width: newWidth, height: max(newHeight, screenHeight)) - scrollView.minimumZoomScale = 1.0 - scrollView.maximumZoomScale = max(screenHeight / newHeight, kMaxScale) - scrollView.zoomScale = 1.0 - } - - @objc private func panGestureAction(_ recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .ended: - scrollView.isScrollEnabled = true - if imageView.frame.origin.y - origFrame.origin.y > kMaxDropHeight { - dismisAnimationWithGestureView(recognizer.view) - } else { - resetFrame() - } - case .cancelled: - resetFrame() - case .changed: - let point = recognizer.translation(in: recognizer.view) - updatePanFrame(point) - default: - break - } - } - - @objc private func singleGestureAction(_ recognizer: UITapGestureRecognizer) { - if let stateLabel = stateLabel, !stateLabel.isHidden { - retryAction() - return - } - if delegate?.singleTapZoomView(imageView) ?? false { - dismisAnimationWithGestureView(recognizer.view) - } - } - - @objc private func doubleGestureAction(_ recognizer: UITapGestureRecognizer) { - if scrollView.zoomScale > scrollView.minimumZoomScale + 0.5 { - scrollView.setZoomScale(scrollView.minimumZoomScale, animated: true) - } else { - let tapPoint = recognizer.location(in: recognizer.view) - scrollView.zoom(to: zoomRectForScale(2.0, withTapPoint: tapPoint), animated: true) - } - } - - @objc private func longPressGestureAction(_ recognizer: UILongPressGestureRecognizer) { - if recognizer.state == .began { - delegate?.longPressZoomView(self) - } - } - - private func retryAction() { - resetErrorNoticeView() - - if let imageUrl = imageUrl, let imageModel = imageModel { - loadImageWithImageUrl(imageUrl, animated: imageModel.sourceRect.size.width > 0) - } - } - - private func updatePanFrame(_ point: CGPoint) { - var scale = 1.0 - if point.y >= 0 && point.y < 360 { - scale = 1 - point.y / 410 - scrollView.backgroundColor = UIColor.black.withAlphaComponent(1 - point.y / 250.0) - delegate?.setVCViewBackgroundColor(1 - point.y / 200.0) - } else if point.y > 360 { - scale = 1 - 360 / 410.0 - } - - let radiusWidth = imageView.frame.width - (imageModel?.sourceRect.width ?? 0) - let newWidth = radiusWidth > 10 ? origFrame.width * scale : imageView.frame.width - let newHeight = radiusWidth > 10 ? origFrame.height * scale : imageView.frame.height - let offsetX = origFrame.width - newWidth - - imageView.frame = CGRect( - x: origFrame.origin.x + point.x + offsetX / 2.0, - y: origFrame.origin.y + point.y, - width: newWidth, - height: newHeight - ) - } - - private func resetFrame() { - isUserInteractionEnabled = false - UIView.animate(withDuration: 0.2) { [weak self] in - guard let self = self else { return } - self.imageView.frame = self.origFrame - self.scrollView.backgroundColor = UIColor.black.withAlphaComponent(1.0) - self.delegate?.setVCViewBackgroundColor(1.0) - } completion: { [weak self] _ in - self?.isUserInteractionEnabled = true - } - } - - private func dismisAnimationWithGestureView(_ view: UIView?) { - delegate?.dismisAnimation(imageView, toFrame: imageModel?.sourceRect ?? .zero) - isHidden = true - } - - private func zoomRectForScale(_ scale: CGFloat, withTapPoint point: CGPoint) -> CGRect { - let touchX = point.x / scrollView.zoomScale - let touchY = point.y / scrollView.zoomScale - let x = touchX + scrollView.contentOffset.x - frame.width / (2 * scale) - let y = touchY + scrollView.contentOffset.y - frame.height / (2 * scale) - return CGRect(x: x, y: y, width: frame.width / scale, height: frame.height / scale) - } - - // MARK: - UIScrollViewDelegate - - func scrollViewDidZoom(_ scrollView: UIScrollView) { - imageView.center = centerOfScrollViewContent(scrollView) - } - - func viewForZooming(in scrollView: UIScrollView) -> UIView? { - return imageView - } - - func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { - isUserInteractionEnabled = false - } - - func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { - isUserInteractionEnabled = true - } - - private func centerOfScrollViewContent(_ scrollView: UIScrollView) -> CGPoint { - let offsetX = scrollView.bounds.width > scrollView.contentSize.width ? (scrollView.bounds.width - scrollView.contentSize.width) * 0.5 : 0 - let offsetY = scrollView.bounds.height > scrollView.contentSize.height ? (scrollView.bounds.height - scrollView.contentSize.height) * 0.5 : 0 - return CGPoint(x: scrollView.contentSize.width * 0.5 + offsetX, y: scrollView.contentSize.height * 0.5 + offsetY) - } - - func prepareForReuse() { - displayIndex = 0 - resetErrorNoticeView() - progress?.removeFromSuperview() - progress = nil - imageView.alpha = 1.0 - imageView.image = nil - imageModel = nil - delegate = nil - updateImageViewFrame() - } - - // MARK: - UIGestureRecognizerDelegate - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return scrollView.isScrollEnabled - } - - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if let pan = gestureRecognizer as? UIPanGestureRecognizer { - let velocity = pan.velocity(in: pan.view) - if abs(velocity.x) < abs(velocity.y) && velocity.y > 0 && scrollView.contentOffset.y < 10 { - scrollView.isScrollEnabled = false - return true - } - } - return false - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowseProgressView.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowseProgressView.swift deleted file mode 100644 index ba16b6f..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowseProgressView.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// EGProgressView.swift -// Crush -// -// Created by Leon on 2025/7/26. -// - -import UIKit - -class PhotoBrowseProgressView: UIView { - var progress: CGFloat = 0 { - didSet { - setNeedsDisplay() - isHidden = progress >= 1 - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .clear - layer.cornerRadius = 5 - clipsToBounds = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { return } - let xCenter = rect.width * 0.5 - let yCenter = rect.height * 0.5 - backgroundColor?.setFill() - UIColor.white.setStroke() - context.setLineWidth(5) - context.setLineCap(.round) - let to = -CGFloat.pi * 0.5 + progress * CGFloat.pi * 2 + 0.05 - let radius = min(rect.width, rect.height) * 0.5 - 10 - context.addArc(center: CGPoint(x: xCenter, y: yCenter), radius: radius, startAngle: -CGFloat.pi * 0.5, endAngle: to, clockwise: false) - context.strokePath() - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserController.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserController.swift deleted file mode 100644 index 931a0d0..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserController.swift +++ /dev/null @@ -1,577 +0,0 @@ -// -// PhotoBrowserController.swift -// Crush -// -// Created by Leon on 2025/7/26. -// - -import Photos -import SnapKit -import UIKit -import Combine - -enum PhotoBrowserType { - /// 普通查看大图,无其他额外操作 - case normal - /// 我的AI角色图片 - case roleMine - /// 他人的AI角色图片 - case roleOthersInIm - /// 浏览他人相册 & Meet 查看大图 - case roleOthersInAlbum - /// 聊天背景图选择(AI生图) - case chatBackgroundGeneratedSelect - /// 设置聊天背景图 - case chatBackgroundSet -} - -class PhotoBrowserController: UIViewController, UIScrollViewDelegate, BrowseImageZoomViewDelegate { - private let kScrollLeftAndRightSpace: CGFloat = 0 - @Published var currentIndex: Int = 0 - private var imageCount: Int = 0 - private var scrollView: UIScrollView! - var titleView: NavigationView! - private var titleTitleStackH: UIStackView! - var titleLockIcon: EPIconTertiaryButton! // 🔒 - var titleunlockedIcon: EPIconPrimaryButton! // 🔓 + 主题背景色 - private var countLabel: UILabel? - private var deleteButton: UIButton? - - // Data - private var visibleZoomViews: Set = [] - private var reusableZoomViews: Set = [] - var imageModels: [PhotoBrowserModel] = [] - - @Published var type: PhotoBrowserType = .normal - private var cancellables = Set() - - // Flag - var isRequesting = false - - // MARK: - Commone通用业务Views - var bottomGradientContainer: GradientView! - var bottomGradientOperateStackV: UIStackView! - - // MARK: Role Mine - /// 解锁方式:免费或coin解锁 - var rolePhotoUnlockEntry: RolePhotoUnlockEntryView! - var setDefaultEntry: RolePhotoSetDefaultEntryView! - var moreButton: EPIconGhostButton? - - // MARK: Role See others - - var roleOthersContainer: SelectiveDeliveryEventsView? - var roleOthersCenterLock: UIImageView? - var iconLabel: CLIconLabel? - var iconUnlockButton: StyleButton? - - // MARK: 通用的地步操作view - /// 居中底部显示一个chipButton - var bottomCommonOperateContainer: SelectiveDeliveryEventsView? - var operateChipButton : EPChipContrastButton? - var likeView: HeartLikeCountView? - - // MARK: Chatbackground set - var setBackgroundDisableButton: StyleButton! - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .black - setupScrollView() - loadTitleView() - - // 其他业务通用的一些容器、View - setupCommonContainers() - setupOprateViews() - - setupEvent() - } - - private func setupEvent(){ - $type.sink {[weak self] type in - if type == .roleMine || type == .chatBackgroundSet{ - self?.moreButton?.isHidden = false - }else{ - self?.moreButton?.isHidden = true - } - }.store(in: &cancellables) - - $currentIndex.sink {[weak self] index in - self?.reloadStates(index: index) - }.store(in: &cancellables) - - WalletCore.shared.$balance.sink {[weak self] balance in - if let priceLabel = self?.iconLabel { -// let balance = balance.balance ?? 0 -// let coin = Coin(cents: balance) - priceLabel.contentLabel.text = balance.displayBalance() - } - - }.store(in: &cancellables) - } - - // MARK: - SetupViews - private func setupScrollView() { - scrollView = UIScrollView() - scrollView.isHidden = true - view.addSubview(scrollView) - view.sendSubviewToBack(scrollView) - - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.contentInsetAdjustmentBehavior = .never - scrollView.isPagingEnabled = true - scrollView.bounces = true - scrollView.backgroundColor = .clear - scrollView.delegate = self - - scrollView.snp.makeConstraints { make in - make.edges.equalTo(UIEdgeInsets(top: 0, left: -kScrollLeftAndRightSpace, bottom: 0, right: kScrollLeftAndRightSpace)) - make.width.equalTo(UIScreen.main.bounds.width + kScrollLeftAndRightSpace) - make.height.equalTo(UIScreen.main.bounds.height) - } - } - - private func loadTitleView() { - guard titleView == nil else { - titleView?.alpha = 1 - return - } - - titleView = NavigationView() - titleView.bgView.alpha = 0 - titleView.setupBackButtonCloseIcon() - titleView.clipsToBounds = false - view.addSubview(titleView) - titleView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(UIWindow.statusBarHeight + 44) - } - let gradient = CLSystemToken.gradient(token: .cob) - let gradientUnderTitle = GradientView(colors: [gradient.secondColor!, gradient.firstColor!], gradientType: .topToBottom) // [UIColor.c.cob.withAlphaComponent(0), UIColor.c.cbd] - titleView.insertSubview(gradientUnderTitle, at: 0) - gradientUnderTitle.snp.makeConstraints { make in - make.leading.trailing.top.equalToSuperview() - make.height.equalTo(140) - } - titleView.tapBackButtonAction = {[weak self] in - self?.backButtonAction() - } - - titleTitleStackH = { - let v = UIStackView() - v.spacing = 8 - v.alignment = .center - titleView.addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(44) - make.centerX.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - titleLockIcon = { - // EPIconTertiaryButton - let v = EPIconTertiaryButton(radius: .rectangle, iconSize: .small, iconCode: .iconPrivate) - titleTitleStackH.addArrangedSubview(v) - v.isHidden = true - return v - }() - - titleunlockedIcon = { - let v = EPIconPrimaryButton(radius: .rectangle, iconSize: .small, iconCode: .iconPublic) - titleTitleStackH.addArrangedSubview(v) - v.isHidden = true - return v - }() - - countLabel = { - let v = UILabel() - titleTitleStackH.addArrangedSubview(v) - v.textColor = .white - v.font = .t.ttm - v.textAlignment = .center - return v - }() - } - - // MARK: - 各项业务对应的view - private func setupCommonContainers(){ - bottomGradientContainer = { - let gradient = CLSystemToken.gradient(token: .cob) - let gradientUnderTitle = GradientView(colors: [gradient.firstColor!, gradient.secondColor!], gradientType: .topToBottom) // [UIColor.c.cob.withAlphaComponent(0), UIColor.c.cbd] - view.addSubview(gradientUnderTitle) - gradientUnderTitle.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - } - return gradientUnderTitle - }() - - bottomGradientOperateStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 16 - bottomGradientContainer!.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(48) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom * 0.5) - } - return v - }() - - bottomGradientContainer?.isHidden = true - } - - private func setupOprateViews() { - switch type { - case .roleMine: - createOperateViewOfRoleMine() - case .roleOthersInIm, .roleOthersInAlbum: - createOperateViewOfOthers() - createBottomCommonOperateView() - case .chatBackgroundSet: - createChatBackgroundSetViews() - case .chatBackgroundGeneratedSelect: - createBottomCommonOperateView() - setupChatBackgroundSelectView() - default: - break - } - } - - private func createBottomCommonOperateView(){ - bottomCommonOperateContainer = { - let v = SelectiveDeliveryEventsView() - view.insertSubview(v, belowSubview: titleView) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - operateChipButton = { - let v = EPChipContrastButton() - v.iconCode = .like - v.addTarget(self, action: #selector(tapOperateChipButton(_:)), for: .touchUpInside) - bottomCommonOperateContainer?.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) - } - v.isHidden = true - return v - }() - - likeView = { - let v = HeartLikeCountView(viewSize: .xxl) - bottomCommonOperateContainer?.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) - } - v.likeButton.addTarget(self, action: #selector(tapLikeButton), for: .touchUpInside) - v.isHidden = true - return v - }() - - bottomCommonOperateContainer?.isHidden = true - roleOthersContainer?.isHidden = true - } - - - private func setupChatBackgroundSelectView(){ - #warning("to do") - //operateChipButton.text = "Select" - } - - - // MARK: - 看图控件 - func setupViews() { - visibleZoomViews.forEach { view in - view.prepareForReuse() - view.removeFromSuperview() - reusableZoomViews.insert(view) - } - visibleZoomViews.removeAll() - - imageCount = imageModels.count - - let width = UIScreen.main.bounds.width - let height = UIScreen.main.bounds.height - scrollView.contentOffset = CGPoint(x: (width + kScrollLeftAndRightSpace) * CGFloat(currentIndex), y: 0) - scrollView.contentSize = CGSize(width: (width + kScrollLeftAndRightSpace) * CGFloat(imageCount), height: height) - setupZoomView(at: currentIndex) - reloadViews() - } - - private func setupZoomView(at index: Int) { - guard index < imageModels.count, index >= 0 else { return } - let model = imageModels[index] - let zoomView = BrowseImageZoomView(imageModel: model) - zoomView.delegate = self - let width = UIScreen.main.bounds.width - let height = UIScreen.main.bounds.height - zoomView.frame = CGRect( - x: (width + kScrollLeftAndRightSpace) * CGFloat(index) + kScrollLeftAndRightSpace, - y: 0, - width: width, - height: height - ) - zoomView.displayIndex = index - scrollView.addSubview(zoomView) - visibleZoomViews.insert(zoomView) - } - - private func reloadZoomView(_ zoomView: BrowseImageZoomView, at index: Int) { - let width = UIScreen.main.bounds.width - let height = UIScreen.main.bounds.height - guard !imageModels.isEmpty else { return } - let safeIndex = min(index, imageModels.count - 1) - - zoomView.frame = CGRect( - x: (width + kScrollLeftAndRightSpace) * CGFloat(safeIndex) + kScrollLeftAndRightSpace, - y: 0, - width: width, - height: height - ) - zoomView.reloadImage(imageModels[safeIndex]) - zoomView.delegate = self - zoomView.displayIndex = safeIndex - scrollView.addSubview(zoomView) - visibleZoomViews.insert(zoomView) - reusableZoomViews.remove(zoomView) - } - - // MARK: - Public - func reloadViews() { - guard imageCount > 1 else { return } - - var hasFrontDisplay = false - var hasAfterDisplay = false - - visibleZoomViews.forEach { zoomView in - if zoomView.displayIndex == currentIndex + 1 { - hasAfterDisplay = true - } - if zoomView.displayIndex == currentIndex - 1 { - hasFrontDisplay = true - } - if abs(zoomView.displayIndex - currentIndex) > 1 { - zoomView.prepareForReuse() - zoomView.removeFromSuperview() - reusableZoomViews.insert(zoomView) - } - } - - visibleZoomViews.subtract(reusableZoomViews) - - if currentIndex + 1 < imageCount, !hasAfterDisplay { - if let zoomView = reusableZoomViews.first { - reloadZoomView(zoomView, at: currentIndex + 1) - } else { - setupZoomView(at: currentIndex + 1) - } - } - - if currentIndex - 1 >= 0, !hasFrontDisplay { - if let zoomView = reusableZoomViews.first { - reloadZoomView(zoomView, at: currentIndex - 1) - } else { - setupZoomView(at: currentIndex - 1) - } - } - } - - func reloadCurrentZoomImage(){ - if let zoomView = visibleZoomViews.first { - reloadZoomView(zoomView, at: currentIndex) - } - } - - @objc func backButtonAction() { - let zoomView = visibleZoomViews.first { view in - guard currentIndex < imageModels.count else { return false } - return view.imageModel == imageModels[currentIndex] - } - - if let zoomView = zoomView { - zoomView.alpha = 0 - hideAnimation(with: zoomView.imageView, toRect: zoomView.imageModel?.sourceRect ?? .zero) - } else { - UIView.animate(withDuration: 0.3) { [weak self] in - self?.setVCViewBackgroundColor(0) - } completion: { _ in - PhotoBrowserManager.shared.hidePhotoBrowser() - } - } - } - - private func showAnimation(_ model: PhotoBrowserModel) { - let imageView = UIImageView(frame: model.sourceRect) - view.addSubview(imageView) - - imageView.contentMode = model.imageContentMode - imageView.image = model.image ?? model.placeHolder - imageView.backgroundColor = .black - imageView.clipsToBounds = true - if imageView.image == nil { - imageView.isHidden = true - } - - var imageWidth = imageView.image?.size.width ?? 1 - var imageHeight = imageView.image?.size.height ?? 1 - let scale = imageHeight / imageWidth - if imageWidth < imageWidth && imageHeight >= UIScreen.main.bounds.height { - imageHeight = UIScreen.main.bounds.height - imageWidth = imageHeight / scale - } else { - imageWidth = UIScreen.main.bounds.width - imageHeight = imageWidth * scale - } - - let hasSourceRect = model.sourceRect.size.width > 0 - - if hasSourceRect { - UIView.animate(withDuration: 0.3) { [weak self] in - guard let self = self else { return } - imageView.backgroundColor = .black - self.view.backgroundColor = .black - imageView.center = CGPoint(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.height * 0.5) - imageView.bounds = CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight) - } completion: { [weak self] _ in - guard let self = self else { return } - self.scrollView.isHidden = false - self.view.backgroundColor = .black - imageView.removeFromSuperview() - self.reloadCountLabel(hidden: self.type == .normal) - } - } else { - imageView.backgroundColor = .black - view.backgroundColor = .black - imageView.center = CGPoint(x: UIScreen.main.bounds.width * 0.5, y: UIScreen.main.bounds.height * 0.5) - imageView.bounds = CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight) - scrollView.isHidden = false - view.backgroundColor = .black - imageView.removeFromSuperview() - reloadCountLabel(hidden: type == .normal) - } - } - - private func hideAnimation(with imageView: UIImageView, toRect rect: CGRect) { - let newImageView = UIImageView(frame: PhotoBrowserModel.getViewRectForScreen(with: imageView)) - view.addSubview(newImageView) - - newImageView.contentMode = .scaleAspectFit - newImageView.image = imageView.image - newImageView.backgroundColor = .black - newImageView.clipsToBounds = true - - UIView.animate(withDuration: 0.3) { [weak self] in - self?.setVCViewBackgroundColor(0) - newImageView.contentMode = .scaleAspectFill - newImageView.backgroundColor = .black - newImageView.frame = rect.size.width > 0 ? rect : newImageView.frame - newImageView.alpha = rect.size.width > 0 ? 0.6 : 0 - } completion: { _ in - PhotoBrowserManager.shared.hidePhotoBrowser() - newImageView.removeFromSuperview() - } - } - - // MARK: - Helper - /// ⚠️此方法好像有问题 - private func getCurrentZoomView() -> BrowseImageZoomView? { - let zoomView = visibleZoomViews.first { view in - guard currentIndex < imageModels.count else { return false } - return view.imageModel == imageModels[currentIndex] - } - return zoomView - } - - func getCurrentZoomView(byModel: PhotoBrowserModel) -> BrowseImageZoomView? { - let zoomView = visibleZoomViews.first { view in - guard currentIndex < imageModels.count else { return false } - return view.imageModel == byModel - } - return zoomView - } - - // MARK: - Functions - - func reloadCountLabel(hidden: Bool) { - if hidden || imageCount <= 1 { - countLabel?.alpha = 0 - return - } - - countLabel?.alpha = 1 - countLabel?.text = "\(currentIndex + 1)/\(imageCount)" - view.bringSubviewToFront(countLabel!) - } - - func reloadStates(index: Int){ - guard imageModels.count > 0 else{ - return - } - let model = imageModels[index] - //let album = model.aiAlbum - reloadStatesByModel(model: model) - } - - - - // MARK: - Public - - func setupView(with imageModels: [PhotoBrowserModel], currentIndex: Int) { - guard !imageModels.isEmpty else { return } - self.currentIndex = currentIndex - imageCount = imageModels.count - self.imageModels = imageModels - showAnimation(imageModels[currentIndex]) - setupViews() - } - - // MARK: - RoleBrowse about - - // MARK: - UIScrollViewDelegate - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let index = Int(scrollView.contentOffset.x / (UIScreen.main.bounds.width + kScrollLeftAndRightSpace)) - // dlog("照片index: \(index)") - guard index != currentIndex else { return } - currentIndex = min(index, imageCount - 1) - reloadViews() - reloadCountLabel(hidden: false) - } - - // MARK: - BrowseImageZoomViewDelegate - - func dismisAnimation(_ zoomImageView: UIImageView, toFrame frame: CGRect) { - hideAnimation(with: zoomImageView, toRect: frame) - } - - func setVCViewBackgroundColor(_ alpha: CGFloat) { - view.backgroundColor = UIColor.black.withAlphaComponent(alpha) - titleView?.alpha = alpha - bottomGradientContainer?.alpha = alpha - deleteButton?.alpha = alpha - roleOthersContainer?.alpha = alpha - bottomCommonOperateContainer?.alpha = alpha - } - - func singleTapZoomView(_ zoomImageView: UIImageView) -> Bool { - return true - } - - @discardableResult - func longPressZoomView(_ zoomView: BrowseImageZoomView) -> Bool { - return true - } - - deinit { - print("♻️EGPhotoBrowserController dealloc") - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerEvent.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerEvent.swift deleted file mode 100644 index 522af52..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerEvent.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// PhotoBrowserControllerEvent.swift -// Crush -// -// Created by Leon on 2025/9/23. -// - -extension PhotoBrowserController{ - func reloadStatesByModel(model:PhotoBrowserModel?){ - guard let browserModel = model else{return} - - if type == .roleMine{ - let unlockPrice = browserModel.aiAlbum.unlockPrice - - if let lockStatus = browserModel.aiAlbum.lockStatus{ - titleLockIcon.isHidden = lockStatus == .unlock - }else{ - titleLockIcon.isHidden = true - } - - - - setDefaultEntry.isHidden = false - let isDefault = browserModel.aiAlbum.isDefault.boolValue - setDefaultEntry.setupIsDefault(isDefault) - - rolePhotoUnlockEntry.setupCoinUnlock(coin: unlockPrice) - if isDefault{ - rolePhotoUnlockEntry.isHidden = true - }else{ - rolePhotoUnlockEntry.isHidden = false - } - - }else if type == .chatBackgroundSet{ - guard let background = browserModel.chatBackground else{return} - - if background.isDefault.boolValue{ - // 隐藏导航栏more 删除 - moreButton?.isHidden = true - }else{ - moreButton?.isHidden = false - } - - setDefaultEntry.isHidden = background.isSelected.boolValue - setBackgroundDisableButton.isHidden = !background.isSelected.boolValue - - }else if type == .roleOthersInAlbum{ - guard let album = browserModel.aiAlbum else{return} - - getCurrentZoomView(byModel: browserModel)?.scrollView.isScrollEnabled = true - if let lockStatus = album.lockStatus, lockStatus == .locked{ - // 🚩分支: 已上锁 - roleOthersContainer?.isHidden = false - bottomCommonOperateContainer?.isHidden = true - likeView?.isHidden = true - titleunlockedIcon.isHidden = true - - getCurrentZoomView(byModel: browserModel)?.scrollView.isScrollEnabled = false - let price = browserModel.aiAlbum.unlockPrice - let attributeString = StyleButton.getUnlockAttributeTitleByCoin(coin: price, string: "unlock") - iconUnlockButton?.setAttributedTitle(attributeString, for: .normal) - }else{ - // 🚩分支: 可看的图片 - - roleOthersContainer?.isHidden = true - bottomCommonOperateContainer?.isHidden = false - likeView?.isHidden = false - - if album.lockStatus == .unlock{ - // 🔓上锁的图片已解锁, 导航栏解锁标志显示 - titleunlockedIcon.isHidden = false - }else{ - // 普通图片 - titleunlockedIcon.isHidden = true - } - - likeView?.bind(aiAlbum: album) - } - - } - else if type == .roleOthersInIm{ - guard let model = browserModel.sessionModel else{ - return - } - - bottomGradientContainer.isHidden = true - - let price = model.baseRemoteInfo?.customAttachment?.unlockPrice ?? 0 - if price > 0{ - roleOthersContainer?.isHidden = false - let attributeString = StyleButton.getUnlockAttributeTitleByCoin(coin: price, string: "unlock") - iconUnlockButton?.setAttributedTitle(attributeString, for: .normal) - }else{ - roleOthersContainer?.isHidden = true - } - } - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerExtChatBackground.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerExtChatBackground.swift deleted file mode 100644 index a16d0da..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerExtChatBackground.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// PhotoBrowserControllerExtChatBackground.swift -// Crush -// -// Created by Leon on 2025/8/26. -// - -extension PhotoBrowserController{ - - // MARK: Chatbackground Set - func createChatBackgroundSetViews(){ - bottomGradientContainer.isHidden = false - - setDefaultEntry = { - let v = RolePhotoSetDefaultEntryView() - v.setupChatBackgroundSetMode() - bottomGradientOperateStackV.addArrangedSubview(v) - return v - }() - - setBackgroundDisableButton = { - let v = StyleButton() - v.contrastTertiaryLight(size: .large) - v.isEnabled = false - v.setTitle("Set background", for: .normal) - bottomGradientOperateStackV.addArrangedSubview(v) - return v - }() - - moreButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .more) - v.addTarget(self, action: #selector(tapNaviMoreInChatBackgroud(sender:)), for: .touchUpInside) - titleView.rightStackH.addArrangedSubview(v) - titleView.paddingRightForRightStack = 16 - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - v.isHidden = true - return v - }() - - setupChatBackgroundEvents() - } - - // MARK: Events - func setupChatBackgroundEvents(){ - setDefaultEntry.setButton.addTarget(self, action: #selector(tapSetChatBackgrondDefaultButton), for: .touchUpInside) - } - - - // MARK: Action - @objc func tapNaviMoreInChatBackgroud(sender: UIButton) { - let pop = CLPopoverListView() - let rect = view.convert(sender.frame, from: sender.superview) - pop.setupDeletePopover(rect.insetBy(dx: 0.0, dy: -6.0), inView: view) { [weak self] in - self?.doDeleteChatBackground() - } - } - - private func doDeleteChatBackground(){ - -// #warning("test") -// self.imageModels.remove(at: self.currentIndex) -// if self.imageModels.isEmpty { -// PhotoBrowserManager.shared.hidePhotoBrowser() -// } else { -// if self.currentIndex >= self.imageModels.count { -// self.currentIndex -= 1 -// } -// self.setupViews() -// self.reloadCountLabel(hidden: false) -// } -// return - - let imageModel = imageModels[currentIndex] - guard let background = imageModel.chatBackground, let backgroundId = background.backgroundId, backgroundId > 0 else{ - return - } - Hud.showIndicator() - AIRoleProvider.request(.deleteChatBackground(backgroundId: backgroundId), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - guard let `self` = self else { - return - } - self.imageModels.remove(at: self.currentIndex) - if self.imageModels.isEmpty { - PhotoBrowserManager.shared.hidePhotoBrowser() - } else { - if self.currentIndex >= self.imageModels.count { - self.currentIndex -= 1 - } - self.setupViews() - self.reloadCountLabel(hidden: false) - self.reloadStates(index: currentIndex) - } - NotificationCenter.post(name: .chatSettingBackgroundListUpdated) - if background.isSelected.boolValue{ - NotificationCenter.post(name: .chatSettingUpdated) - } - case .failure: - break - } - } - } - - @objc private func tapSetChatBackgrondDefaultButton(){ - let imageModel = imageModels[currentIndex] - - guard let aiUid = imageModel.aiId else{ - return - } - - Hud.showIndicator() - var params = [String: Any]() - params.updateValue(aiUid, forKey: "aiId") - - if let background = imageModel.chatBackground, let backgroundId = background.backgroundId, backgroundId > 0{ - params.updateValue(backgroundId, forKey: "backgroundId") - } - - AIRoleProvider.request(.setDefaultChatBackground(params: params), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - //NotificationCenter.post(name: .chatSettingBackgroundListUpdated) - self?.resetAndMarkSelectedBackground(imgUrl: imageModel.imageUrl) - - PhotoBrowserManager.shared.hidePhotoBrowser() - NotificationCenter.post(name: .chatSettingBackgroundChanged) - NotificationCenter.post(name: .chatSettingUpdated) - case .failure: - break - } - } - } - - // MARK: Helper - private func resetAndMarkSelectedBackground(imgUrl: String?){ - guard let selectedUrl = imgUrl else{return} - for per in imageModels { - if let perImgurl = per.chatBackground?.imgUrl, perImgurl == selectedUrl{ - per.chatBackground?.isSelected = true - }else{ - per.chatBackground?.isSelected = false - } - } - reloadStates(index: currentIndex) - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerExtRole.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerExtRole.swift deleted file mode 100644 index 5e9e301..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserControllerExtRole.swift +++ /dev/null @@ -1,437 +0,0 @@ -// -// PhotoBrowserControllerExtRole.swift -// Crush -// -// Created by Leon on 2025/7/27. -// - -extension PhotoBrowserController { - // MARK: - 🚩My Role - - func createOperateViewOfRoleMine() { - bottomGradientContainer?.isHidden = false - - rolePhotoUnlockEntry = { - let v = RolePhotoUnlockEntryView() - bottomGradientOperateStackV.addArrangedSubview(v) - return v - }() - setDefaultEntry = { - let v = RolePhotoSetDefaultEntryView() - bottomGradientOperateStackV.addArrangedSubview(v) - return v - }() - - moreButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .more) - v.addTarget(self, action: #selector(tapNaviMoreInMyAIAlbum(sender:)), for: .touchUpInside) - titleView.rightStackH.addArrangedSubview(v) - titleView.paddingRightForRightStack = 16 - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - setupRoleEvents() - } - - func setupRoleEvents() { - rolePhotoUnlockEntry.editUnlockWayAction = { [weak self] in - self?.tapToSetupUnlockWay() - } - - setDefaultEntry.setButton.addTarget(self, action: #selector(tapSetRoleDefaultButton), for: .touchUpInside) - } - - // MARK: Action - - func tapToSetupUnlockWay() { - let vc = RolePhotoUnlockWaySetController() - // 获取当前item - let imageModel = imageModels[currentIndex] - - vc.browseModel = imageModel - vc.priceUpdateAction = { [weak self] price in - let unlockPrice = price ?? 0 - imageModel.aiAlbum.unlockPrice = unlockPrice - if unlockPrice > 0 { - imageModel.aiAlbum.isDefault = false - } - - self?.reloadStatesByModel(model: imageModel) - } - navigationController?.pushViewController(vc, animated: true) - } - - @objc func tapNaviMoreInMyAIAlbum(sender: UIButton) { - let pop = CLPopoverListView() - let rect = view.convert(sender.frame, from: sender.superview) - pop.setupDeletePopover(rect.insetBy(dx: 0.0, dy: -6.0), inView: view) { [weak self] in - self?.deleteAlbumOfMyAIRoleTapAction() - } - } - - @objc private func tapSetRoleDefaultButton() { - let imageModel = imageModels[currentIndex] - let unlockPrice = imageModel.aiAlbum.unlockPrice - - if unlockPrice > 0{ - let alert = Alert(title: "默认图片", text: "设置为默认图片后,图片的解锁方式只能为“免费”") - let action1 = AlertAction(title: "Confirm", actionStyle: .confirm) {[weak self] in - self?.doRequestSetRoleDefaultAlbum() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - }else{ - doRequestSetRoleDefaultAlbum() - } - } - - // MARK: Functions - private func doRequestSetRoleDefaultAlbum(){ - let imageModel = imageModels[currentIndex] - - let aiUid = imageModel.aiId - let albumId = imageModel.aiAlbum.albumId - - Hud.showIndicator() - AIRoleProvider.request(.aiRolePhotoDefaultSet(aiUid: aiUid, albumId: albumId), modelType: AnyCodable.self) { [weak self] result in - Hud.hideIndicator() - switch result { - case .success: - // dlog("set default results") - imageModel.aiAlbum.isDefault = true - self?.reloadStatesByModel(model: imageModel) - self?.setDefaultEntry.setupIsDefault(true) - NotificationCenter.post(name: .aiRoleAlbumPhotoInfoChanged) - NotificationCenter.post(name: .aiRoleInfoChanged) // 主页背景要更新 - case .failure: - break - } - } - } - - @objc func deleteAlbumOfMyAIRoleTapAction() { - guard currentIndex >= 0, currentIndex < imageModels.count else { return } - - let model = imageModels[currentIndex] - if model.aiAlbum.isDefault.boolValue{ - let alert = Alert(title: "删除图片", text: "不可删除封面默认图片,该图片会作为在个人主页头图,卡片主图,聊天背景") - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) - alert .addAction(action1) - alert.show() - return - } - - - let alert = Alert(title: "删除图片", text: "图片删除后不可恢复。已经付费解锁过该图片的用户依然可以在角色的相册中看到该图片。") - let action1 = AlertAction(title: "Delete", actionStyle: .destructive) {[weak self] in - self?.deleteTapActionDone() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } - - private func deleteTapActionDone() { - let model = imageModels[currentIndex] - model.deleteTapBlock?(model) { [weak self] isSuccess in - guard let self = self else { return } - if isSuccess { - self.imageModels.remove(at: self.currentIndex) - if self.imageModels.isEmpty { - PhotoBrowserManager.shared.hidePhotoBrowser() - } else { - if self.currentIndex >= self.imageModels.count { - self.currentIndex -= 1 - } - self.setupViews() - self.reloadCountLabel(hidden: false) - } - } - } - } - - // MARK: - 🚩Other's Role - - func createOperateViewOfOthers() { - roleOthersContainer = { - let v = SelectiveDeliveryEventsView() - v.backgroundColor = .clear - view.insertSubview(v, belowSubview: titleView) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - // roleOthersContainer?.isUserInteractionEnabled = false - - roleOthersCenterLock = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .iconPrivate, size: CGSize(width: 40, height: 40), color: .white) - roleOthersContainer?.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - let bottomStackH = { - let v = UIStackView() - v.spacing = 48 - v.alignment = .center - roleOthersContainer?.addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(48) - make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom * 0.5) - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - - iconUnlockButton = { - let v = StyleButton() - v.setContentHuggingPriority(UILayoutPriority(rawValue: 246), for: .horizontal) - v.primary(size: .large) - v.addTarget(self, action: #selector(tapUnlockPhoto), for: .touchUpInside) - bottomStackH.addArrangedSubview(v) - return v - }() - - iconLabel = { - let v = CLIconLabel() - v.iconImageView.image = UIImage.icon32Diamond - v.iconSize = CGSize(width: 20, height: 20) - roleOthersContainer?.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(bottomStackH) - make.leading.equalToSuperview().offset(24) - make.trailing.lessThanOrEqualTo(bottomStackH.snp.leading).offset(-8) - } - return v - }() - - iconLabel?.contentLabel.text = "1234" - let attributeString = StyleButton.getUnlockAttributeTitleByCoin(coin: 123, string: "unlock") - iconUnlockButton?.setAttributedTitle(attributeString, for: .normal) - } - - @objc func tapUnlockPhoto() { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{ - PhotoBrowserManager.shared.hidePhotoBrowser() - return - } - - let photoModel = imageModels[currentIndex] - - var params = [String: Any]() - if let album = photoModel.aiAlbum { - if let aiId = photoModel.aiId { - params.updateValue(aiId, forKey: "aiId") - } - let albumId = album.albumId - params.updateValue(albumId, forKey: "albumId") - - } else if let model = photoModel.sessionModel { - if let aiId = photoModel.aiId { - params.updateValue(aiId, forKey: "aiId") - } - if let albumId = model.baseRemoteInfo?.customAttachment?.albumId { - params.updateValue(albumId, forKey: "albumId") - } - if let messageServerId = model.v2msg?.messageServerId { - params.updateValue(messageServerId, forKey: "messageServerId") - } - } else { - return - } - - // 测试解锁 - let imgTest = "https://hhb.crushlevel.ai/dev/role/17581021489486439.jpg" -// if var album = photoModel.aiAlbum{ -// -// album.lockStatus = .unlock -// album.imgUrl = nil -// album.img1 = "https://sub.crushlevel.ai/files/b/test/439076604280833/0aec65fdd92c404a9e3f8acac8e2c1ba.jpg?x-oss-process=image/resize,w_468,h_600&v=1&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9zdWIuY3J1c2hsZXZlbC5haS9maWxlcy9iL3Rlc3QvNDM5MDc2NjA0MjgwODMzLzBhZWM2NWZkZDkyYzQwNGE5ZTNmOGFjYWM4ZTJjMWJhLmpwZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSx3XzQ2OCxoXzYwMCZ2PTEiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3NTgyMjgzNjd9fX1dfQ__&Signature=Jv5jE4d072OkFyYq5dxa3unXVaLWSNRddJEIIm42j4zmg36Yq8HGxVXz4X954ERmujm8zCluiVGMbIx-EH9JlQ1s~zbhgOo-lliaDGl977EsCVLBPsf639WFpdCXqnf3QAM7LwhuCbmdi~PBybJhKS31nEKoS53idOB4JyEXXp9wv3RpVjDoL4JF8C22H8k5gJ0hpt3NhfZiTSbsSRJBApNHfCKAFm6PEKQ8dAQ6f7rRxHwPTkbhHi~fRUahuJI10Ti-8t2Hem4akTV4plmgz9chsLCfHBNh5WDrwzoCZRDDljpaNcwpkZ5Mew23seKphb5iLxHgVP3WGAwXb0BoRA__&Key-Pair-Id=K35ZQ246Y37QV8" -// photoModel.imageUrl = album.img1 -// album.unlockPrice = 0 -// photoModel.aiAlbum = album -// self.reloadStates(index: self.currentIndex) -// self.reloadCurrentZoomImage() -// } -// return - -// #warning("test") -// if let model = photoModel.sessionModel { -// Hud.hideInidcator() -// model.baseRemoteInfo?.customAttachment?.unlockPrice = 0 -// model.baseRemoteInfo?.customAttachment?.url = imgTest -// -// photoModel.imageUrl = imgTest -// photoModel.sessionModel = model -// -// // Reload -// self.reloadStates(index: self.currentIndex) -// self.reloadCurrentZoomImage() -// } -// return - - Hud.showIndicator() - ChatProvider.request(.aiUnlockAlbumImg(params: params), modelType: AIAlbumUnlockImages.self) { [weak self] result in - Hud.hideIndicator() - switch result { - case let .success(unlockData): - guard let img1 = unlockData?.img1 else { - PhotoBrowserManager.shared.hidePhotoBrowser() - return - } - - // 🚩"1.发送通知、2.更新当前大图UI" - - // 更新临时数据 - guard let self = self else { return } - if let album = photoModel.aiAlbum { - album.lockStatus = .unlock - album.img1 = img1 - album.imgUrl = img1 - album.unlockPrice = 0 - - photoModel.imageUrl = album.img1 - photoModel.aiAlbum = album - - // Reload - self.reloadStates(index: self.currentIndex) - self.reloadCurrentZoomImage() - - NotificationCenter.post(name: .aiRoleAlbumPhotoInfoChanged, object: album) - }else if let model = photoModel.sessionModel { - model.baseRemoteInfo?.customAttachment?.unlockPrice = 0 - model.baseRemoteInfo?.customAttachment?.url = img1 - - photoModel.imageUrl = img1 - photoModel.sessionModel = model - - // Reload - self.reloadStates(index: self.currentIndex) - self.reloadCurrentZoomImage() - - NotificationCenter.post(name: .aiRoleAlbumPhotoInfoChanged, object: model) - }else{ - assert(false, "need update") - } - - - WalletCore.shared.refreshWallet() - - case .failure: - break - } - } - } - - @objc func tapOperateChipButton(_ sender: EPChipContrastButton) { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{ - PhotoBrowserManager.shared.hidePhotoBrowser() - return - } - } - - @objc func tapLikeButton() { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{ - PhotoBrowserManager.shared.hidePhotoBrowser() - return - } - - let photoModel = imageModels[currentIndex] - guard let likeView = likeView else { return } - // 在主页、IM中,这里是 点赞 - - if let album = photoModel.aiAlbum { - let albumId = album.albumId - - if album.likedStatus == .liked { - // 取消点赞 - album.likedStatus = .cancel - album.likedCount = max((album.likedCount ?? 0) - 1, 0) - PhotosViewModel.shared.album = album - likeView.bind(aiAlbum: album) - AIRoleProvider.request(.aiRolePhotoLikeOrNo(albumId: albumId, likedStatus: .cancel), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, albumId: albumId, isLike: false) - } - } else { - // 点赞 - likeView.playLotteLike { _ in - guard var album = photoModel.aiAlbum else { return } - album.likedStatus = .liked - album.likedCount = (album.likedCount ?? 0) + 1 - PhotosViewModel.shared.album = album - likeView.bind(aiAlbum: album) - } - - AIRoleProvider.request(.aiRolePhotoLikeOrNo(albumId: albumId, likedStatus: .liked), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, albumId: albumId, isLike: true) - } - } - } - } - - // 统一处理请求结果 - private func handleLikeRequestResult(result: Result, albumId: Int, isLike: Bool) { - isRequesting = false - - let photoModel = imageModels[currentIndex] - guard let likeView = likeView, let album = photoModel.aiAlbum else { return } - likeView.likeButton.isEnabled = true - - switch result { - case .success: - // 请求成功,无需额外处理 - break - case .failure: - // 请求失败,恢复状态 - - if isLike { - // 点赞失败,恢复为未点赞状态 - album.likedStatus = .cancel - album.likedCount = max((album.likedCount ?? 0) - 1, 0) - } else { - // 取消点赞失败,恢复为点赞状态 - album.likedStatus = .liked - album.likedCount = (album.likedCount ?? 0) + 1 - } - PhotosViewModel.shared.album = album - likeView.bind(aiAlbum: album) - } - } - - // MARK: - Helper - - /// like : 💎 20 unlock - func getUnlockAttributeTitleByCoin(coin: Int) -> NSAttributedString { - let text = " \(coin) unlock" - let attributedString = NSMutableAttributedString() - if let iconImage = UIImage(named: "icon_32_diamond") { // 替换为你的图标名称 - let attachment = NSTextAttachment() - attachment.image = iconImage - - let iconSize = CGSize(width: 24, height: 24) // 调整为你需要的大小 - attachment.bounds = CGRect(origin: .init(x: 0, y: -4), size: iconSize) - - let iconAttributedString = NSAttributedString(attachment: attachment) - attributedString.append(iconAttributedString) - } - - // 添加文字并设置样式 - let textAttributedString = text.withAttributes([ - .font(.t.tll), // 设置字体 - .textColor(.white), - ]) - attributedString.append(textAttributedString) - return attributedString - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserManager.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserManager.swift deleted file mode 100644 index 7a130ef..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserManager.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// PhotoBrowserManager.swift -// Crush -// -// Created by Leon on 2025/7/26. -// - -import UIKit - -class PhotoBrowserManager { - public static let shared = PhotoBrowserManager() - private var browserWindow: UIWindow? - - private init() {} - - private func getBrowserWindow() -> UIWindow { -// if browserWindow == nil { -// browserWindow = UIWindow(frame: UIScreen.main.bounds) -// browserWindow?.backgroundColor = .clear -// } -// return browserWindow! - guard let windowScene = UIApplication.shared.connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene - else { - return UIWindow.applicationKey! - } - let window = UIWindow(windowScene: windowScene) - window.frame = UIScreen.main.bounds - return window - } - - static func hidePhotoBrowserWindow() { - shared.hidePhotoBrowser() - } - - func showPhotoBrowser(with models: [PhotoBrowserModel], currentIndex: Int) { - showPhotoBrowser(with: models, currentIndex: currentIndex, type: .normal) - } - - func showPhotoBrowser(with models: [PhotoBrowserModel], currentIndex: Int, type: PhotoBrowserType) { - guard !models.isEmpty else { return } - let safeIndex = max(0, min(currentIndex, models.count - 1)) - - let window = getBrowserWindow() - window.windowLevel = .statusBar - 2 - window.isHidden = false - self.browserWindow = window - - let browserVC = PhotoBrowserController() - browserVC.type = type - browserVC.setupView(with: models, currentIndex: safeIndex) - let navc = CLNavigationController(rootViewController: browserVC) - window.rootViewController = navc//browserVC - } - - func setupPhotoBrowserType(_ type: PhotoBrowserType) { - guard let browserVC = browserWindow?.rootViewController as? PhotoBrowserController else { return } - browserVC.type = type - } - - func hidePhotoBrowser() { - browserWindow?.isHidden = true - browserWindow?.rootViewController = nil - } - - deinit { - print("♻️PhotoBrowserManager dealloc") - } -} - -enum ImageBrowser { - static func show(models: [PhotoBrowserModel], index: Int, type: PhotoBrowserType) { - PhotoBrowserManager.shared.showPhotoBrowser(with: models, currentIndex: index, type: type) - } - - static func hide() { - PhotoBrowserManager.hidePhotoBrowserWindow() - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserModel.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserModel.swift deleted file mode 100644 index 8eab431..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/PhotoBrowserModel.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// PhotoBrowserModel.swift -// Crush -// -// Created by Leon on 2025/7/26. -// - -import UIKit - -class PhotoBrowserModel:NSObject { - var image: UIImage? - var imageContentMode: UIView.ContentMode = .scaleAspectFill - var imageUrl: String? - var placeHolder: UIImage? - var sourceRect: CGRect = .zero - var tagInfo: Any? - - var deleteTapBlock: ((PhotoBrowserModel, @escaping (Bool) -> Void) -> Void)? - var likeTapBlock: ((PhotoBrowserModel, @escaping (Bool) -> Void) -> Void)? - - // MARK: 🚩 一些扩展字段 - // 相册处的模型 - var aiAlbum: AlbumPhotoItem! - /// aiId - var aiId: Int? - var chatBackground: IMChatBackground? - // IM中 - var sessionModel: SessionBaseModel? - - override init() { - sourceRect = .zero - imageContentMode = .scaleAspectFill - } - - var computedSourceRect: CGRect { - if sourceRect == .zero { - if let image = image { - let width = image.size.width - let height = image.size.height - return CGRect( - x: UIScreen.main.bounds.width * 0.5 - width * 0.5, - y: UIScreen.main.bounds.height * 0.5 - height * 0.5, - width: width, - height: height - ) - } else { - return CGRect( - x: UIScreen.main.bounds.width * 0.5 - 40, - y: UIScreen.main.bounds.height * 0.5 - 40, - width: 80, - height: 80 - ) - } - } - return sourceRect - } - - static func getViewRectForScreen(with view: UIView) -> CGRect { - guard let window = UIApplication.shared.windows.first else { return .zero } - return view.superview?.convert(view.frame, to: window) ?? .zero - } - - deinit { - print("♻️EGPhotoBrowserModel dealloc") - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoBrowser/Views/RolePhotoBrowseViews.swift b/crush/Crush/Src/Components/Photo/PhotoBrowser/Views/RolePhotoBrowseViews.swift deleted file mode 100644 index 8bdb5b8..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoBrowser/Views/RolePhotoBrowseViews.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// RolePhotoBrowseViews.swift -// Crush -// -// Created by Leon on 2025/7/26. -// - -import UIKit - -class RolePhotoUnlockEntryView: UIView { - var iconImageView: UIImageView! - var unlockWayLabel: UILabel! - var unlockWayButton: UIButton! - - var editUnlockWayAction: (() -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csedn - layer.cornerRadius = 16 - layer.masksToBounds = true - - snp.makeConstraints { make in - make.height.equalTo(56) - } - - let titleTipLabel = { - let v = UILabel() - v.text = "Unlock Method" - v.font = .t.tts - v.textColor = .text - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - } - return v - }() - titleTipLabel.isHidden = false - - iconImageView = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .iconOrderRemark, size: CGSize(width: 16, height: 16), color: .white) - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - make.width.height.equalTo(16) - } - return v - }() - - unlockWayLabel = { - let v = UILabel() - v.text = "Free" - v.font = .t.tlm - v.textColor = .text - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(iconImageView.snp.leading).offset(-12) - make.centerY.equalToSuperview() - } - return v - }() - - unlockWayButton = { - let v = UIButton() - v.addTarget(self, action: #selector(unlockWayButtonAction), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - -// #warning("test") -// setupCoinUnlock(coin: 20) - } - - @objc func unlockWayButtonAction() { - editUnlockWayAction?() - } - - public func setupCoinUnlock(coin: Int) { - if coin == 0{ - unlockWayLabel.attributedText = nil - unlockWayLabel.text = "Free" - return - } - - let attributedString = NSMutableAttributedString() - - let text = " \(String.thousandString(float:Double(coin)/100.0))" - // 添加图标 - if let iconImage = UIImage(named: "icon_16_diamond") { // 替换为你的图标名称 - let attachment = NSTextAttachment() - attachment.image = iconImage - - let iconSize = CGSize(width: 16, height: 16) // 调整为你需要的大小 - attachment.bounds = CGRect(origin: .init(x: 0, y: -2), size: iconSize) - - // 将图标添加到 AttributedString - let iconAttributedString = NSAttributedString(attachment: attachment) - attributedString.append(iconAttributedString) - } - - // 添加文字并设置样式 - let textAttributedString = text.withAttributes([ - .font(.t.tlm), // 设置字体 - .textColor(.white) - ]) - attributedString.append(textAttributedString) - - unlockWayLabel.text = nil - unlockWayLabel.attributedText = attributedString - } -} - -class RolePhotoSetDefaultEntryView: UIView { - var setButton: StyleButton! - var titleLabel: UILabel! - var descriptionLabel: LineSpaceLabel! - var leftLabelsStackV: UIStackView! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csedn - layer.cornerRadius = 16 - layer.masksToBounds = true - - setButton = { - let v = StyleButton() - v.primary(size: .small) - v.setTitle("Set", for: .normal) - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - leftLabelsStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 4 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - make.trailing.equalTo(setButton.snp.leading).offset(-12) - } - - return v - }() - - titleLabel = { - let v = UILabel() - v.text = "Set as Default" - v.font = .t.tts - v.textColor = .text - v.numberOfLines = 0 - leftLabelsStackV.addArrangedSubview(v) - return v - }() - - descriptionLabel = { - let v = LineSpaceLabel() - v.text = "Only apply to the background of your chat with the character" - let typo = CLSystemToken.typography(token: .tbs) - v.config(typo) - v.textColor = .text - leftLabelsStackV.addArrangedSubview(v) - return v - }() - - titleLabel.isHidden = false - descriptionLabel.isHidden = false - } - - // MARK: Public - func setupIsDefault(_ isDefault: Bool){ - if isDefault{ - setButton.isHidden = true - descriptionLabel.isHidden = false - leftLabelsStackV.snp.remakeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - make.trailing.equalToSuperview().offset(-16) - } - }else{ - setButton.isHidden = false - descriptionLabel.isHidden = true - leftLabelsStackV.snp.remakeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - make.trailing.equalTo(setButton.snp.leading).offset(-12) - } - } - } - - func setupChatBackgroundSetMode(){ - titleLabel.text = "Set background" - descriptionLabel.text = "Only apply to the background of your chat with the character" - } -} diff --git a/crush/Crush/Src/Components/Photo/PhotoPicker/ImagePicker.swift b/crush/Crush/Src/Components/Photo/PhotoPicker/ImagePicker.swift deleted file mode 100755 index 1a40690..0000000 --- a/crush/Crush/Src/Components/Photo/PhotoPicker/ImagePicker.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// ImagePicker.swift -// LegendTeam -// -// Created by 梁博 on 13/12/21. -// - -import TZImagePickerController -import UIKit - -class ImagePicker: TZImagePickerController { - override init!(maxImagesCount: Int, delegate: TZImagePickerControllerDelegate!) { - super.init(maxImagesCount: maxImagesCount, columnNumber: 3, delegate: delegate, pushPhotoPickerVc: true) - - let mainColor = UIColor.c.cpn // UIColor.hex120E1B - let mainTextColor = UIColor.white - - sortAscendingByModificationDate = false - allowPickingVideo = false - allowTakePicture = true - allowCameraLocation = false - allowTakeVideo = true - showSelectedIndex = true - showPhotoCannotSelectLayer = true - cannotSelectLayerColor = mainColor.withAlphaComponent(0.5) - modalPresentationStyle = .fullScreen - statusBarStyle = .lightContent // UIApplication.shared.statusBarStyle - - allowPreview = maxImagesCount > 1 - - TZImagePickerConfig.sharedInstance().gifPreviewMaxImagesCount = 1 - - #warning("to do") - naviTitleFont = .t.ttl // UIFont.fredokaOne(size: 18) - naviBgColor = mainColor - // normalBgColor = mainColor - - naviTitleColor = mainTextColor -// naviSubTitleColor = mainTextColor -// previewTextFont = .t.tbs//UIFont.popSemiBold(size: 12) -// doneBtnTextFont = .t.tbs//UIFont.popSemiBold(size: 14) - - oKButtonTitleColorNormal = mainTextColor - oKButtonTitleColorDisabled = .c.ctd // UIColor.hex727085 - -// let btnImage = UIImage.gradientHImageWithSize(size: CGSize(width: 80, height: 28), colors: [UIColor.hex8E28FF.cgColor, UIColor.hex3F03FF.cgColor]) -// doneBtnNormalImage = btnImage -// doneBtnDisableImage = UIColor.hex302F3D.toImage() - - preferredLanguage = "en" // "en" - doneBtnTitleStr = "Confirm" // R.string.localizable.confirm.localized() - cancelBtnTitleStr = "Cancel" // R.string.localizable.cancel.localized() - previewBtnTitleStr = "Preview" // R.string.localizable.preview.localized() - fullImageBtnTitleStr = "Original Photo" // R.string.localizable.original_photo.localized() - - photoOriginDefImage = R.image.icon_tag_gray_default() - photoOriginSelImage = R.image.icon_tag_selected() - photoDefImage = R.image.icon_tag_gray_default() - photoSelImage = R.image.icon_tag_selected_bg() - takePictureImage = R.image.icon_album_camera() - - photoPickerPageUIConfigBlock = { (_ collectionView: UICollectionView?, - _ bottomToolBar: UIView?, - _ previewButton: UIButton?, - _ originalPhotoButton: UIButton?, - _ originalPhotoLabel: UILabel?, - _ doneButton: UIButton?, - _: UIImageView?, - _: UILabel?, - _ divideLine: UIView?) in - collectionView?.backgroundColor = mainColor - bottomToolBar?.backgroundColor = mainColor - previewButton?.setTitleColor(mainTextColor, for: .normal) - previewButton?.setTitleColor(UIColor.gray, for: .disabled) - originalPhotoButton?.setTitleColor(UIColor.gray, for: .normal) - originalPhotoButton?.setTitleColor(mainTextColor, for: .selected) - originalPhotoLabel?.textColor = mainTextColor - divideLine?.backgroundColor = UIColor.black -// doneButton?.setBackgroundImage(btnImage, for: .normal) - doneButton?.backgroundColor = .clear - } - - photoPreviewPageUIConfigBlock = { (_: UICollectionView?, - _: UIView?, - _: UIButton?, - _: UIButton?, - _: UILabel?, - _: UIView?, - _ originalPhotoButton: UIButton?, - _ originalPhotoLabel: UILabel?, - _: UIButton?, - _: UIImageView?, - _: UILabel?) in - originalPhotoButton?.setTitleColor(UIColor.gray, for: .normal) - originalPhotoButton?.setTitleColor(mainTextColor, for: .selected) - originalPhotoLabel?.textColor = mainTextColor - // originalPhotoLabel?.font - } - - albumCellDidLayoutSubviewsBlock = { (_ cell: TZAlbumCell?, - _: UIImageView?, - _ titleLabel: UILabel?) in - - titleLabel?.font = .t.ttl // UIFont.popSemiBold(size: 16) - titleLabel?.textColor = .white - cell?.backgroundColor = mainColor - } - - setupNoti() - } - - @available(*, unavailable) - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - - // MARK: - noti - - private func setupNoti() { - NotificationCenter.default.addObserver(self, selector: #selector(notiTzImagePicker(noti:)), name: AppNotificationName.tzImagePickerNoti.notificationName, object: nil) - } - - @objc private func notiTzImagePicker(noti: Notification) { - guard let userInfo = noti.userInfo as? Dictionary else { return } - - if let toast = userInfo["toast"] as? String { - UIWindow.key?.hideToast() - UIWindow.key?.makeToast(toast) - } - } - - deinit { - NotificationCenter.default.removeObserver(self) - } -} diff --git a/crush/Crush/Src/Components/Skin/CLBaseTokens.swift b/crush/Crush/Src/Components/Skin/CLBaseTokens.swift deleted file mode 100644 index 65e1af0..0000000 --- a/crush/Crush/Src/Components/Skin/CLBaseTokens.swift +++ /dev/null @@ -1,209 +0,0 @@ -// -// CLUI.swift -// Crush -// -// Created by Leon on 2025/7/11. -// - -import Foundation - -let EPConfigShadowPrefix = "@SHA:" -let EPConfigGradientPrefix = "@GRA:" - -class CLBaseTokens { - static var shared = CLBaseTokens() - // MARK: - Properties - - var globalTokens: [String: Any]? - var sysDarkTokens: [String: Any]? - var sysLightTokens: [String: Any]? - - var globalKeys: [String]? - var sysDarkKeys: [String]? - var sysLightKeys: [String]? - - // MARK: - Singleton - // static let shared = CLBaseTokens() - - init() { - initialTokens() - } - - // MARK: - Initial - private func initialTokens() { - // 加载 global tokens - if let json1Path = Bundle.main.path(forResource: "token_global", ofType: "json"), - let json1Data = try? Data(contentsOf: URL(fileURLWithPath: json1Path)), - let dict1 = try? JSONSerialization.jsonObject(with: json1Data, options: []) as? [String: Any] { - globalTokens = dict1 - globalKeys = dict1.keys.sorted() - //assert(dict1 != nil, "json should be valid") - } - - // 加载 sysDark tokens - if let json2Path = Bundle.main.path(forResource: "token_sys_normal", ofType: "json"), - let json2Data = try? Data(contentsOf: URL(fileURLWithPath: json2Path)), - let dict2 = try? JSONSerialization.jsonObject(with: json2Data, options: []) as? [String: Any] { - sysDarkTokens = dict2 - sysDarkKeys = dict2.keys.sorted() - } - - assert(sysDarkTokens != nil) - } - - // MARK: - Public Methods - - /// ⚠️ 只针对单 token 的情况,保留此方法,不推荐使用 - func getTokenByKey(_ key: String) -> String { - var value: String? - - if let sysDarkKeys = sysDarkKeys, sysDarkKeys.contains(key) { - value = sysDarkTokens?[key] as? String - } - - if value == nil, let globalKeys = globalKeys, globalKeys.contains(key) { - value = globalTokens?[key] as? String - } - - guard let finalValue = value else { - print("\(key) UI Token Key may doesn't exist") - return "" - } - - if finalValue.contains("$") { - let keyNew = finalValue.replacingOccurrences(of: "$", with: "") - return getTokenByKey(keyNew) - } - - return finalValue - } - - func getTokensByKey(_ key: String) -> [String] { - var value: String? - - - if let sysKeys = sysDarkKeys { - if sysKeys.contains(key){ - value = sysDarkTokens?[key] as? String - } - } - - if value == nil, let globalKeys = globalKeys, globalKeys.contains(key) { - value = globalTokens?[key] as? String - } - - guard let finalValue = value else { - print("❌\(key)❌ UI Token Key doesn't exist ❌❌") - return [] - } - - let params = finalValue.components(separatedBy: ",") - var arrayM: [String] = [] - - for per in params { - let dealPer = per.trimmingCharacters(in: .whitespacesAndNewlines) - if dealPer.contains("$") { - var keyNew = dealPer - keyNew = keyNew.replacingOccurrences(of: EPConfigShadowPrefix, with: "") - keyNew = keyNew.replacingOccurrences(of: EPConfigGradientPrefix, with: "") - - if keyNew.contains("&") { - let keys = keyNew.components(separatedBy: "&") - var multi: [String] = [] - - for perKey in keys { - let keyNew2 = perKey.replacingOccurrences(of: "$", with: "") - let value = getTokenByKey(keyNew2) - multi.append(value) - } - - let covertValues = multi.joined(separator: "&") - arrayM.append(covertValues) - } else { - var values: [String] = [] - var tempKey = keyNew - while tempKey.contains("$") { - tempKey = tempKey.replacingOccurrences(of: "$", with: "") - values = getTokensByKey(tempKey) - arrayM.append(contentsOf: values) - } - } - } else { - arrayM.append(dealPer) - } - } - - return arrayM - } - - // MARK: - Always Dark - - /// ⚠️ 只针对单 token 的情况,保留此方法,不推荐使用 - func getDarkToken(_ key: String) -> String { - var value: String? - - if let sysDarkKeys = sysDarkKeys, sysDarkKeys.contains(key) { - value = sysDarkTokens?[key] as? String - } - - if value == nil, let globalKeys = globalKeys, globalKeys.contains(key) { - value = globalTokens?[key] as? String - } - - guard let finalValue = value else { - print("❌\(key)❌ UI Token Key doesn't exist ❌❌") - return "" - } - - if finalValue.contains("$") { - let keyNew = finalValue.replacingOccurrences(of: "$", with: "") - return getTokenByKey(keyNew) - } - - return finalValue - } - - func getDarkTokens(_ key: String) -> [String] { - var value: String? - - if let sysDarkKeys = sysDarkKeys, sysDarkKeys.contains(key) { - value = sysDarkTokens?[key] as? String - } - - if value == nil, let globalKeys = globalKeys, globalKeys.contains(key) { - value = globalTokens?[key] as? String - } - - guard let finalValue = value else { - print("❌\(key)❌ UI Token Key doesn't exist ❌❌") - return [] - } - - let params = finalValue.components(separatedBy: ",") - var arrayM: [String] = [] - - for per in params { - let dealPer = per.trimmingCharacters(in: .whitespacesAndNewlines) - if dealPer.contains("$") { - var keyNew = dealPer - keyNew = keyNew.replacingOccurrences(of: EPConfigShadowPrefix, with: "") - keyNew = keyNew.replacingOccurrences(of: EPConfigGradientPrefix, with: "") - - var values: [String] = [] - while keyNew.contains("$") { - keyNew = keyNew.replacingOccurrences(of: "$", with: "") - values = getTokensByKey(keyNew) - arrayM.append(contentsOf: values) - } - } else { - arrayM.append(dealPer) - } - } - - return arrayM - } -} - - - - diff --git a/crush/Crush/Src/Components/Skin/CLGlobalTokens.swift b/crush/Crush/Src/Components/Skin/CLGlobalTokens.swift deleted file mode 100644 index 56475c3..0000000 --- a/crush/Crush/Src/Components/Skin/CLGlobalTokens.swift +++ /dev/null @@ -1,307 +0,0 @@ -// -// CLGlobalTokens.swift -// Crush -// -// Created by Leon on 2025/8/14. -// - -enum CLEnumGlobalToken: String, CaseIterable { - init?(caseName: String) { - self = CLEnumGlobalToken.allCases.first(where: { "\($0)" == caseName }) ?? .glo_color_orange_0 - } - - case glo_color_orange_0 = "glo.color.orange.0" - case glo_color_orange_10 = "glo.color.orange.10" - case glo_color_orange_20 = "glo.color.orange.20" - case glo_color_orange_30 = "glo.color.orange.30" - case glo_color_orange_40 = "glo.color.orange.40" - case glo_color_orange_50 = "glo.color.orange.50" - case glo_color_orange_60 = "glo.color.orange.60" - case glo_color_orange_70 = "glo.color.orange.70" - case glo_color_orange_80 = "glo.color.orange.80" - case glo_color_orange_90 = "glo.color.orange.90" - case glo_color_yellow_0 = "glo.color.yellow.0" - case glo_color_yellow_10 = "glo.color.yellow.10" - case glo_color_yellow_20 = "glo.color.yellow.20" - case glo_color_yellow_30 = "glo.color.yellow.30" - case glo_color_yellow_40 = "glo.color.yellow.40" - case glo_color_yellow_50 = "glo.color.yellow.50" - case glo_color_yellow_60 = "glo.color.yellow.60" - case glo_color_yellow_70 = "glo.color.yellow.70" - case glo_color_yellow_80 = "glo.color.yellow.80" - case glo_color_yellow_90 = "glo.color.yellow.90" - case glo_color_grass_0 = "glo.color.grass.0" - case glo_color_grass_10 = "glo.color.grass.10" - case glo_color_grass_20 = "glo.color.grass.20" - case glo_color_grass_30 = "glo.color.grass.30" - case glo_color_grass_40 = "glo.color.grass.40" - case glo_color_grass_50 = "glo.color.grass.50" - case glo_color_grass_60 = "glo.color.grass.60" - case glo_color_grass_70 = "glo.color.grass.70" - case glo_color_grass_80 = "glo.color.grass.80" - case glo_color_grass_90 = "glo.color.grass.90" - case glo_color_green_0 = "glo.color.green.0" - case glo_color_green_10 = "glo.color.green.10" - case glo_color_green_20 = "glo.color.green.20" - case glo_color_green_30 = "glo.color.green.30" - case glo_color_green_40 = "glo.color.green.40" - case glo_color_green_50 = "glo.color.green.50" - case glo_color_green_60 = "glo.color.green.60" - case glo_color_green_70 = "glo.color.green.70" - case glo_color_green_80 = "glo.color.green.80" - case glo_color_green_90 = "glo.color.green.90" - case glo_color_mint_0 = "glo.color.mint.0" - case glo_color_mint_10 = "glo.color.mint.10" - case glo_color_mint_20 = "glo.color.mint.20" - case glo_color_mint_30 = "glo.color.mint.30" - case glo_color_mint_40 = "glo.color.mint.40" - case glo_color_mint_50 = "glo.color.mint.50" - case glo_color_mint_60 = "glo.color.mint.60" - case glo_color_mint_70 = "glo.color.mint.70" - case glo_color_mint_80 = "glo.color.mint.80" - case glo_color_mint_90 = "glo.color.mint.90" - case glo_color_sky_0 = "glo.color.sky.0" - case glo_color_sky_10 = "glo.color.sky.10" - case glo_color_sky_20 = "glo.color.sky.20" - case glo_color_sky_30 = "glo.color.sky.30" - case glo_color_sky_40 = "glo.color.sky.40" - case glo_color_sky_50 = "glo.color.sky.50" - case glo_color_sky_60 = "glo.color.sky.60" - case glo_color_sky_70 = "glo.color.sky.70" - case glo_color_sky_80 = "glo.color.sky.80" - case glo_color_sky_90 = "glo.color.sky.90" - case glo_color_blue_0 = "glo.color.blue.0" - case glo_color_blue_10 = "glo.color.blue.10" - case glo_color_blue_20 = "glo.color.blue.20" - case glo_color_blue_30 = "glo.color.blue.30" - case glo_color_blue_40 = "glo.color.blue.40" - case glo_color_blue_50 = "glo.color.blue.50" - case glo_color_blue_60 = "glo.color.blue.60" - case glo_color_blue_70 = "glo.color.blue.70" - case glo_color_blue_80 = "glo.color.blue.80" - case glo_color_blue_90 = "glo.color.blue.90" - case glo_color_violet_0 = "glo.color.violet.0" - case glo_color_violet_10 = "glo.color.violet.10" - case glo_color_violet_20 = "glo.color.violet.20" - case glo_color_violet_30 = "glo.color.violet.30" - case glo_color_violet_40 = "glo.color.violet.40" - case glo_color_violet_50 = "glo.color.violet.50" - case glo_color_violet_60 = "glo.color.violet.60" - case glo_color_violet_70 = "glo.color.violet.70" - case glo_color_violet_80 = "glo.color.violet.80" - case glo_color_violet_90 = "glo.color.violet.90" - case glo_color_purple_0 = "glo.color.purple.0" - case glo_color_purple_10 = "glo.color.purple.10" - case glo_color_purple_20 = "glo.color.purple.20" - case glo_color_purple_30 = "glo.color.purple.30" - case glo_color_purple_40 = "glo.color.purple.40" - case glo_color_purple_50 = "glo.color.purple.50" - case glo_color_purple_60 = "glo.color.purple.60" - case glo_color_purple_70 = "glo.color.purple.70" - case glo_color_purple_80 = "glo.color.purple.80" - case glo_color_purple_90 = "glo.color.purple.90" - case glo_color_magenta_0 = "glo.color.magenta.0" - case glo_color_magenta_10 = "glo.color.magenta.10" - case glo_color_magenta_20 = "glo.color.magenta.20" - case glo_color_magenta_30 = "glo.color.magenta.30" - case glo_color_magenta_40 = "glo.color.magenta.40" - case glo_color_magenta_50 = "glo.color.magenta.50" - case glo_color_magenta_60 = "glo.color.magenta.60" - case glo_color_magenta_70 = "glo.color.magenta.70" - case glo_color_magenta_80 = "glo.color.magenta.80" - case glo_color_magenta_90 = "glo.color.magenta.90" - case glo_color_red_0 = "glo.color.red.0" - case glo_color_red_10 = "glo.color.red.10" - case glo_color_red_20 = "glo.color.red.20" - case glo_color_red_30 = "glo.color.red.30" - case glo_color_red_40 = "glo.color.red.40" - case glo_color_red_50 = "glo.color.red.50" - case glo_color_red_60 = "glo.color.red.60" - case glo_color_red_70 = "glo.color.red.70" - case glo_color_red_80 = "glo.color.red.80" - case glo_color_red_90 = "glo.color.red.90" - case glo_color_grey_0 = "glo.color.grey.0" - case glo_color_grey_10 = "glo.color.grey.10" - case glo_color_grey_20 = "glo.color.grey.20" - case glo_color_grey_30 = "glo.color.grey.30" - case glo_color_grey_40 = "glo.color.grey.40" - case glo_color_grey_50 = "glo.color.grey.50" - case glo_color_grey_60 = "glo.color.grey.60" - case glo_color_grey_70 = "glo.color.grey.70" - case glo_color_grey_80 = "glo.color.grey.80" - case glo_color_grey_90 = "glo.color.grey.90" - case glo_color_grey_100 = "glo.color.grey.100" - case glo_color_white = "glo.color.white" - case glo_color_black = "glo.color.black" - case glo_transparent_t0 = "glo.transparent.t0" - case glo_transparent_t2 = "glo.transparent.t2" - case glo_transparent_t4 = "glo.transparent.t4" - case glo_transparent_t6 = "glo.transparent.t6" - case glo_transparent_t8 = "glo.transparent.t8" - case glo_transparent_t12 = "glo.transparent.t12" - case glo_transparent_t15 = "glo.transparent.t15" - case glo_transparent_t20 = "glo.transparent.t20" - case glo_transparent_t25 = "glo.transparent.t25" - case glo_transparent_t30 = "glo.transparent.t30" - case glo_transparent_t45 = "glo.transparent.t45" - case glo_transparent_t60 = "glo.transparent.t60" - case glo_transparent_t65 = "glo.transparent.t65" - case glo_transparent_t85 = "glo.transparent.t85" - case glo_transparent_t100 = "glo.transparent.t100" - case glo_deg_ltr = "glo.deg.ltr" - case glo_deg_ttb = "glo.deg.ttb" - case glo_deg_lttrb = "glo.deg.lttrb" - case glo_font_family_sys = "glo.font.family.sys" - case glo_font_family_sys_italic = "glo.font.family.sys.italic" - case glo_font_family_numDisplay = "glo.font.family.numDisplay" - case glo_font_family_num = "glo.font.family.num" - case glo_font_family_display = "glo.font.family.display" - case glo_font_size_64 = "glo.font.size.64" - case glo_font_size_48 = "glo.font.size.48" - case glo_font_size_36 = "glo.font.size.36" - case glo_font_size_24 = "glo.font.size.24" - case glo_font_size_20 = "glo.font.size.20" - case glo_font_size_18 = "glo.font.size.18" - case glo_font_size_16 = "glo.font.size.16" - case glo_font_size_14 = "glo.font.size.14" - case glo_font_size_12 = "glo.font.size.12" - case glo_font_weight_regular = "glo.font.weight.regular" - case glo_font_weight_medium = "glo.font.weight.medium" - case glo_font_weight_semibold = "glo.font.weight.semibold" - case glo_font_weight_bold = "glo.font.weight.bold" - case glo_font_lineheight_size64 = "glo.font.lineheight.size64" - case glo_font_lineheight_size48 = "glo.font.lineheight.size48" - case glo_font_lineheight_size36 = "glo.font.lineheight.size36" - case glo_font_lineheight_size24 = "glo.font.lineheight.size24" - case glo_font_lineheight_size20 = "glo.font.lineheight.size20" - case glo_font_lineheight_size18 = "glo.font.lineheight.size18" - case glo_font_lineheight_size16 = "glo.font.lineheight.size16" - case glo_font_lineheight_size14 = "glo.font.lineheight.size14" - case glo_font_lineheight_size12 = "glo.font.lineheight.size12" - case glo_font_lineheight_size0 = "glo.font.lineheight.size0" - case glo_radio_1_1 = "glo.radio.1.1" - case glo_radio_4_3 = "glo.radio.4.3" - case glo_radio_3_2 = "glo.radio.3.2" - case glo_radio_2_1 = "glo.radio.2.1" - case glo_radio_16_9 = "glo.radio.16.9" - case glo_radius_4 = "glo.radius.4" - case glo_radius_8 = "glo.radius.8" - case glo_radius_12 = "glo.radius.12" - case glo_radius_16 = "glo.radius.16" - case glo_radius_round = "glo.radius.round" - case glo_border_half = "glo.border.half" - case glo_border_1 = "glo.border.1" - case glo_border_2 = "glo.border.2" - case glo_border_4 = "glo.border.4" - case glo_spacing_4 = "glo.spacing.4" - case glo_spacing_8 = "glo.spacing.8" - case glo_spacing_12 = "glo.spacing.12" - case glo_spacing_16 = "glo.spacing.16" - case glo_spacing_24 = "glo.spacing.24" - case glo_spacing_32 = "glo.spacing.32" - case glo_spacing_48 = "glo.spacing.48" - case glo_spacing_64 = "glo.spacing.64" - case glo_spacing_80 = "glo.spacing.80" - case glo_spacing_128 = "glo.spacing.128" -} - -func KeyForGlobalTokenFrom(_ token: CLEnumGlobalToken) -> String { - return token.rawValue -} -class CLGlobalToken { - static let baseTokens = CLBaseTokens.shared - - // MARK: - Color Methods - - static func color(token: CLEnumGlobalToken) -> UIColor? { - let key = KeyForGlobalTokenFrom(token) - let values = baseTokens.getTokensByKey(key) - return EPBaseObject.subGetColorByTokenValues(values) - } - - static func darkColor(token: CLEnumGlobalToken) -> UIColor? { - let key = KeyForGlobalTokenFrom(token) - let values = baseTokens.getDarkTokens(key) - return EPBaseObject.subGetColorByTokenValues(values) - } - - // MARK: - String Method - - static func string(token: CLEnumGlobalToken) -> String? { - let key = KeyForGlobalTokenFrom(token) - return baseTokens.getTokenByKey(key) - } - - // MARK: - Float Method - - static func float(token: CLEnumGlobalToken) -> CGFloat { - let key = KeyForGlobalTokenFrom(token) - let value = baseTokens.getTokenByKey(key) - guard let floatValue = Float(value) else { - print("❌ Invalid float format for key: \(key)") - return 0 - } - return CGFloat(floatValue) - } - - // MARK: - Radius Method - - static func radius(token: CLEnumGlobalToken) -> CGFloat { - let key = KeyForGlobalTokenFrom(token) - let value = baseTokens.getTokenByKey(key) - guard let radius = Float(value) else { - print("❌ Invalid radius format for key: \(key)") - return 0 - } - // print("🎨 radius: \(key) \(radius)") // DLog 替换为 print - return CGFloat(radius) - } - - // MARK: - Border Method - - static func border(token: CLEnumGlobalToken) -> CGFloat { - let key = KeyForGlobalTokenFrom(token) - let value = baseTokens.getTokenByKey(key) - guard let border = Float(value) else { - print("❌ Invalid border format for key: \(key)") - return 0 - } - // print("🎨 border: \(key) \(border)") // DLog 替换为 print - return CGFloat(border) - } - - // MARK: - Shadow Method - - static func shadow(token: CLEnumGlobalToken) -> EPShadow { - let key = KeyForGlobalTokenFrom(token) - let values = baseTokens.getTokensByKey(key) - - guard values.count == 4, values.first?.contains(EPConfigShadowPrefix) == true else { - assertionFailure("Wrong format for shadow token: \(key)") - return EPShadow() - } - - var shadow = EPShadow() - - if let colorStr = values.first?.replacingOccurrences(of: EPConfigShadowPrefix, with: "") { - let color = EPBaseObject.subGetColorByTokenValues([colorStr]) - shadow.color = color - } - - if let opacity = Float(values[1]) { - shadow.opacity = opacity - } - let offXy = values[2].components(separatedBy: "&") - if offXy.count == 2, - let x = Float(offXy[0]), let y = Float(offXy[1]) { - shadow.offset = CGSize(width: CGFloat(x), height: CGFloat(y)) - } - - if let radius = Float(values[3]) { - shadow.radius = CGFloat(radius) - } - - return shadow - } - -} diff --git a/crush/Crush/Src/Components/Skin/CLSystemTokens.swift b/crush/Crush/Src/Components/Skin/CLSystemTokens.swift deleted file mode 100644 index d1c4f3c..0000000 --- a/crush/Crush/Src/Components/Skin/CLSystemTokens.swift +++ /dev/null @@ -1,578 +0,0 @@ -// -// CLSystemTokens.swift -// Crush -// -// Created by Leon on 2025/7/11. -// - -import UIKit - -// MARK: - Convenience functions -extension UIColor { - static var c: SystemColor { - SystemColor() - } -} - -extension UIFont { - static var t: SystemFont { - SystemFont() - } -} - -@dynamicMemberLookup -struct SystemColor { - subscript(dynamicMember key: String) -> UIColor { - guard let token = CLEnumSystemToken.allCases.first(where: { "\($0)" == key }) else { - return .black - } - return CLSystemToken.color(token: token) ?? .red - } - - func systemTokenValue(for key: String) -> String? { - CLEnumSystemToken.allCases.first(where: { "\($0)" == key })?.rawValue - } -} - -@dynamicMemberLookup -struct SystemFont { - subscript(dynamicMember key: String) -> UIFont { - guard let token = CLEnumSystemToken.allCases.first(where: { "\($0)" == key }) else { - return .systemFont(ofSize: 14) - } - return CLSystemToken.font(token: token) - } - - func systemTokenValue(for key: String) -> String? { - CLEnumSystemToken.allCases.first(where: { "\($0)" == key })?.rawValue - } -} - - - -// MARK: - CLEnumSystemToken: Enums and Constants - -enum CLEnumSystemToken: String, CaseIterable { - init?(caseName: String) { - self = CLEnumSystemToken.allCases.first(where: { "\($0)" == caseName }) ?? .cpn - } - - case cpn = "color.primary.normal" - case cph = "color.primary.hover" - case cpp = "color.primary.press" - case cpd = "color.primary.disabled" - case cpvn = "color.primary.variant.normal" - case cpvh = "color.primary.variant.hover" - case cpvp = "color.primary.variant.press" - case cpvd = "color.primary.variant.disabled" - case cpgn = "color.primary.gradient.normal" - case cpgh = "color.primary.gradient.hover" - case cpgp = "color.primary.gradient.press" - case cpgd = "color.primary.gradient.disabled" - case cin = "color.important.normal" - case cih = "color.important.hover" - case cip = "color.important.press" - case cid = "color.important.disabled" - case civn = "color.important.variant.normal" - case civh = "color.important.variant.hover" - case civp = "color.important.variant.press" - case civd = "color.important.variant.disabled" - case cign = "color.important.gradient.normal" - case cigh = "color.important.gradient.hover" - case cigp = "color.important.gradient.press" - case ciopn = "color.important.onpic.normal" - case cposn = "color.positive.normal" - case cposh = "color.positive.hover" - case cposp = "color.positive.press" - case cposd = "color.positive.disabled" - case cposvn = "color.positive.variant.normal" - case cposvh = "color.positive.variant.hover" - case cposvp = "color.positive.variant.press" - case cposvd = "color.positive.variant.disabled" - case cposgn = "color.positive.gradient.normal" - case cposopn = "color.positive.onpic.normal" - case cwn = "color.warning.normal" - case cwh = "color.warning.hover" - case cwp = "color.warning.press" - case cwd = "color.warning.disabled" - case cwvn = "color.warning.variant.normal" - case cwvh = "color.warning.variant.hover" - case cwvp = "color.warning.variant.press" - case cwvd = "color.warning.variant.disabled" - case cwgn = "color.warning.gradient.normal" - case cwopn = "color.warning.onpic.normal" - case cen = "color.emphasis.normal" - case ceh = "color.emphasis.hover" - case cep = "color.emphasis.press" - case ced = "color.emphasis.disabled" - case cevn = "color.emphasis.variant.normal" - case cevh = "color.emphasis.variant.hover" - case cevp = "color.emphasis.variant.press" - case cevd = "color.emphasis.variant.disabled" - case cegn = "color.emphasis.grandient.normal" - case cegh = "color.emphasis.grandient.hover" - case cegp = "color.emphasis.grandient.press" - case cegd = "color.emphasis.grandient.disabled" - case ceopn = "color.emphasis.onpic.normal" - case cbd = "color.background.default" - case cbs = "color.background.specialmap" - case cbdi = "color.background.district" - case csbn = "color.surface.base.normal" - case csbh = "color.surface.base.hover" - case csbp = "color.surface.base.press" - case csbsn = "color.surface.base.specialmap.normal" - case csbsh = "color.surface.base.specialmap.hover" - case csbsp = "color.surface.base.specialmap.press" - case csbsd = "color.surface.base.specialmap.disabled" - case csfn = "color.surface.float.normal" - case csfh = "color.surface.float.hover" - case csfp = "color.surface.float.press" - case cstn = "color.surface.top.normal" - case csth = "color.surface.top.hover" - case cstp = "color.surface.top.press" - case cstd = "color.surface.top.disabled" - case csdn = "color.surface.district.normal" - case csdh = "color.surface.district.hover" - case csdp = "color.surface.district.press" - case csdd = "color.surface.district.disabled" - case csnn = "color.surface.nest.normal" - case csnh = "color.surface.nest.hover" - case csnp = "color.surface.nest.press" - case csnd = "color.surface.nest.disabled" - case csen = "color.surface.element.normal" - case cseh = "color.surface.element.hover" - case csep = "color.surface.element.press" - case csed = "color.surface.element.disabled" - case csedn = "color.surface.element.dark.normal" - case csedh = "color.surface.element.dark.hover" - case csedp = "color.surface.element.dark.press" - case csedd = "color.surface.element.dark.disabled" - case cseln = "color.surface.element.light.normal" - case cselh = "color.surface.element.light.hover" - case cselp = "color.surface.element.light.press" - case cseld = "color.surface.element.light.disabled" - case cswn = "color.surface.white.normal" - case cswh = "color.surface.white.hover" - case cswp = "color.surface.white.press" - case cswd = "color.surface.white.disabled" - case csbn2 = "color.surface.black.normal" - case con = "color.outline.normal" - case coh = "color.outline.hover" - case cop = "color.outline.press" - case cod = "color.outline.disabled" - case copr = "color.overlay.primary" - case cogr = "color.overlay.gradient" - case codr = "color.overlay.dark" - /// graident - case cob = "color.overlay.background" - case cobase = "color.overlay.base" - /// vip gradient - case ccvn = "color.context.vip.normal" - case ctpn = "color.txt.primary.normal" - case ctph = "color.txt.primary.hover" - case ctpp = "color.txt.primary.press" - case ctpd = "color.txt.primary.disabled" - case ctpsn = "color.txt.primary.specialmap.normal" - case ctpsh = "color.txt.primary.specialmap.hover" - case ctpsp = "color.txt.primary.specialmap.press" - case ctpsd = "color.txt.primary.specialmap.disabled" - case ctsn = "color.txt.secondary.normal" - case ctsh = "color.txt.secondary.hover" - case ctsp = "color.txt.secondary.press" - case ctsd = "color.txt.secondary.disabled" - case cttn = "color.txt.tertiary.normal" - case ctg = "color.txt.grass" - case ctd = "color.txt.disabled" - case td = "txt.display" - case tdxl = "txt.display.xl" - case tdl = "txt.display.l" - case tdm = "txt.display.m" - case tds = "txt.display.s" - case thl = "txt.headline.l" - case thm = "txt.headline.m" - case ths = "txt.headline.s" - case ttl = "txt.title.l" - case ttm = "txt.title.m" - case tts = "txt.title.s" - case tbsl = "txt.bodySemibold.l" - case tbsm = "txt.bodySemibold.m" - case tbss = "txt.bodySemibold.s" - case tbl = "txt.body.l" - case tbm = "txt.body.m" - case tbim = "txt.body.italic.m" - case tbs = "txt.body.s" - case tll = "txt.label.l" - case tlm = "txt.label.m" - case tls = "txt.label.s" - case tndxl = "txt.numDisplay.xl" - case tndl = "txt.numDisplay.l" - case tndm = "txt.numDisplay.m" - case tnds = "txt.numDisplay.s" - case tnmxl = "txt.numMonotype.xl" - case tnml = "txt.numMonotype.l" - case tnmm = "txt.numMonotype.m" - case tnms = "txt.numMonotype.s" - case tnmxs = "txt.numMonotype.xs" - case shs = "shadow.s" - case shm = "shadow.m" - case shl = "shadow.l" - case rxs = "radius.xs" - case rs = "radius.s" - case rm = "radius.m" - case rl = "radius.l" - case rxl = "radius.xl" - case rxxl = "radius.xxl" - case rr = "radius.round" - case rp = "radius.pill" - case bd = "border.divider" - case bs = "border.s" - case bm = "border.m" - case bl = "border.l" -} - -func KeyForSystemTokenFrom(_ token: CLEnumSystemToken) -> String { - return token.rawValue -} - -// MARK: - EPShadow - -struct EPShadow { - var color: UIColor? - var opacity: Float = 0 - var offset: CGSize = .zero - var radius: CGFloat = 0 -} - -// MARK: - EPGradient - -struct EPGradient { - var direction: CGPoint = .zero - var firstColor: UIColor? - var secondColor: UIColor? - var thirdColor : UIColor? - - public func toImage(size: CGSize) -> UIImage? { - guard let firstColor = firstColor?.cgColor, let secondColor = secondColor?.cgColor else { - return nil - } - - // 根据 direction 计算 startPoint 和 endPoint - let startPoint: CGPoint - let endPoint: CGPoint - - switch direction { - case CGPoint(x: 0, y: -1): // 从上到下 - startPoint = CGPoint(x: 0.5, y: 0) - endPoint = CGPoint(x: 0.5, y: 1) - case CGPoint(x: 1, y: -1): // 从左上到右下 - startPoint = CGPoint(x: 0, y: 0) - endPoint = CGPoint(x: 1, y: 1) - default: - // 如果 direction 不匹配已知方向,返回 nil 或使用默认方向 - startPoint = CGPoint(x: 0, y: 0.5) - endPoint = CGPoint(x: 1, y: 0.5) // 默认水平渐变 - } - - var colors = [firstColor, secondColor] - if(colors.count == 3){ - if let third = thirdColor?.cgColor{ - colors = [firstColor, secondColor, third] - } - } - - // 调用 gradientImageWithSize 生成图像 - return UIImage.gradientImageWithSize( - size: size, - colors: colors, - startPoint: startPoint, - endPoint: endPoint - ) - } - - public func colors() -> [UIColor]{ - var colors = [UIColor]() - if let first = firstColor{ - colors.append(first) - } - if let second = secondColor{ - colors.append(second) - } - if let third = thirdColor{ - colors.append(third) - } - return colors - } -} - -// MARK: - EPTypography - -struct EPTypography { - var font: UIFont? - var lineHeight: CGFloat = 0 -} - -// MARK: - EPBaseObject - -class EPBaseObject { - static func subGetColorByTokenValues(_ values: [String]) -> UIColor? { - guard !values.isEmpty else { - preconditionFailure("Invalid color values: empty array") - return nil - } - - guard let hex = values.first, hex.hasPrefix("#") else { - print("❌ Invalid color format for values: \(values)") - return nil - } - - let hexString = hex.replacingOccurrences(of: "#", with: "") - var rgb: UInt64 = 0 - Scanner(string: hexString).scanHexInt64(&rgb) - - let red = CGFloat((rgb >> 16) & 0xFF) / 255.0 - let green = CGFloat((rgb >> 8) & 0xFF) / 255.0 - let blue = CGFloat(rgb & 0xFF) / 255.0 - let alpha: CGFloat = values.count > 1 ? CGFloat(Float(values.last!) ?? 1.0) : 1.0 - - return UIColor(red: red, green: green, blue: blue, alpha: alpha) - } -} - -// MARK: - CLSystemToken - -class CLSystemToken { - static let baseTokens = CLBaseTokens.shared - - // MARK: - Color Methods - - static func color(token: CLEnumSystemToken) -> UIColor? { - let key = KeyForSystemTokenFrom(token) - let values = baseTokens.getTokensByKey(key) - return EPBaseObject.subGetColorByTokenValues(values) - } - - static func darkColor(token: CLEnumSystemToken) -> UIColor? { - let key = KeyForSystemTokenFrom(token) - let values = baseTokens.getDarkTokens(key) - return EPBaseObject.subGetColorByTokenValues(values) - } - - // MARK: - String Method - - static func string(token: CLEnumSystemToken) -> String? { - let key = KeyForSystemTokenFrom(token) - return baseTokens.getTokenByKey(key) - } - - // MARK: - Float Method - - static func float(token: CLEnumSystemToken) -> CGFloat { - let key = KeyForSystemTokenFrom(token) - let value = baseTokens.getTokenByKey(key) - guard let floatValue = Float(value) else { - print("❌ Invalid float format for key: \(key)") - return 0 - } - return CGFloat(floatValue) - } - - // MARK: - Radius Method - - static func radius(token: CLEnumSystemToken) -> CGFloat { - let key = KeyForSystemTokenFrom(token) - let value = baseTokens.getTokenByKey(key) - guard let radius = Float(value) else { - print("❌ Invalid radius format for key: \(key)") - return 0 - } - // print("🎨 radius: \(key) \(radius)") // DLog 替换为 print - return CGFloat(radius) - } - - // MARK: - Border Method - - static func border(token: CLEnumSystemToken) -> CGFloat { - let key = KeyForSystemTokenFrom(token) - let value = baseTokens.getTokenByKey(key) - guard let border = Float(value) else { - print("❌ Invalid border format for key: \(key)") - return 0 - } - // print("🎨 border: \(key) \(border)") // DLog 替换为 print - return CGFloat(border) - } - - // MARK: - Shadow Method - - static func shadow(token: CLEnumSystemToken) -> EPShadow { - let key = KeyForSystemTokenFrom(token) - let values = baseTokens.getTokensByKey(key) - - guard values.count == 4, values.first?.contains(EPConfigShadowPrefix) == true else { - assertionFailure("Wrong format for shadow token: \(key)") - return EPShadow() - } - - var shadow = EPShadow() - - if let colorStr = values.first?.replacingOccurrences(of: EPConfigShadowPrefix, with: "") { - let color = EPBaseObject.subGetColorByTokenValues([colorStr]) - shadow.color = color - } - - if let opacity = Float(values[1]) { - shadow.opacity = opacity - } - let offXy = values[2].components(separatedBy: "&") - if offXy.count == 2, - let x = Float(offXy[0]), let y = Float(offXy[1]) { - shadow.offset = CGSize(width: CGFloat(x), height: CGFloat(y)) - } - - if let radius = Float(values[3]) { - shadow.radius = CGFloat(radius) - } - - return shadow - } - - // MARK: - Gradient Method - - static func gradient(token: CLEnumSystemToken) -> EPGradient { -// if token == .colorContextVipNormal { -// assertionFailure("Special style rule, not used in this unified rule, please use another method") -// return gradient(token: .colorPrimaryGradientNormal) -// } - - let key = KeyForSystemTokenFrom(token) - let values = baseTokens.getTokensByKey(key) - - if values.count <= 2 { // || values.first?.contains(EPConfigGradientPrefix) != true - var gradient = EPGradient() - gradient.direction = CGPoint(x: 1, y: 0) - gradient.firstColor = EPBaseObject.subGetColorByTokenValues(values) - gradient.secondColor = gradient.firstColor - return gradient - } - - // 方向+ 2个颜色,或者3个颜色 - guard values.count == 3 || values.count == 4 else { - assertionFailure("Wrong format for gradient token: \(key)") - return EPGradient() - } - - var gradient = EPGradient() - - if let directionStr = values.first { - switch directionStr { - case "LTR": - gradient.direction = CGPoint(x: 1, y: 0) - case "TTB": - gradient.direction = CGPoint(x: 0, y: 1) - case "LTTRB": - gradient.direction = CGPoint(x: 1, y: 1) - default: - assertionFailure("Not handled direction: \(directionStr)") - gradient.direction = .zero - } - } - - let color1Str = values[1].replacingOccurrences(of: EPConfigGradientPrefix, with: "") - let color1Array = color1Str.components(separatedBy: "&") - gradient.firstColor = EPBaseObject.subGetColorByTokenValues(color1Array) - - let color2Str = values[2].replacingOccurrences(of: EPConfigGradientPrefix, with: "") - let color2Array = color2Str.components(separatedBy: "&") - gradient.secondColor = EPBaseObject.subGetColorByTokenValues(color2Array) - - if(values.count == 4){ - let color3Str = values[3].replacingOccurrences(of: EPConfigGradientPrefix, with: "") - let color3Array = color3Str.components(separatedBy: "&") - gradient.thirdColor = EPBaseObject.subGetColorByTokenValues(color3Array) - } - - return gradient - } - - // MARK: - Typography Method - - static func typography(token: CLEnumSystemToken) -> EPTypography { - let key = KeyForSystemTokenFrom(token) - let values = baseTokens.getTokensByKey(key) - - guard values.count == 4 else { - assertionFailure("Wrong format for typography token: \(key)") - return EPTypography() - } - - var typography = EPTypography() - - var fontName = values[0] - guard let fontSize = Int(values[1]), - let weight = Int(values[2]), - let lineHeight = Int(values[3]) else { - assertionFailure("Invalid typography values for token: \(key)") - return EPTypography() - } - - if !fontName.contains("-Italic") { - switch weight { - case 400: - fontName += "-Regular" - case 500: - fontName += "-Medium" - case 600: - fontName += "-SemiBold" - case 700: - fontName += "-Bold" - default: - assertionFailure("Not handled font weight: \(weight)") - } - } - - typography.font = UIFont(name: fontName, size: CGFloat(fontSize)) - typography.lineHeight = CGFloat(lineHeight) - - return typography - } - - // MARK: - Font Method - - static func font(token: CLEnumSystemToken) -> UIFont { - let key = KeyForSystemTokenFrom(token) - let values = baseTokens.getTokensByKey(key) - - guard values.count == 4 else { - assertionFailure("Wrong format for font token: \(key)") - return UIFont.systemFont(ofSize: 14) - } - - var fontName = values[0] - guard let fontSize = Int(values[1]), - let weight = Int(values[2]) else { - assertionFailure("Invalid font values for token: \(key)") - return UIFont.systemFont(ofSize: 14) - } - - if(fontName .isNotBlank){ - fontName = fontName.removingAllWhitespace - } - - if !fontName.contains("-Italic") { - switch weight { - case 400: - fontName += "-Regular" - case 500: - fontName += "-Medium" - case 600: - fontName += "-SemiBold" - case 700: - fontName += "-Bold" - default: - assertionFailure("Not handled font weight: \(weight)") - } - } - - return UIFont(name: fontName, size: CGFloat(fontSize)) ?? UIFont.systemFont(ofSize: 14) - } -} diff --git a/crush/Crush/Src/Components/Skin/UIColorCL.swift b/crush/Crush/Src/Components/Skin/UIColorCL.swift deleted file mode 100644 index 0828c3d..0000000 --- a/crush/Crush/Src/Components/Skin/UIColorCL.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// UIColorCL.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import Foundation -import UIKit - -extension UIColor{ - static var hex120E1B: UIColor { - return .hexString("#120E1B") - } - - static var theme: UIColor { - return .c.cpn - } - - static var text: UIColor{ - return .c.ctpn - } - - static var desc: UIColor{ - return .c.ctsn - } - - static var disable: UIColor{ - return .c.ctpd - } - - static var line: UIColor{ - return .c.con - } -} diff --git a/crush/Crush/Src/Components/Skin/UICommonSetup.swift b/crush/Crush/Src/Components/Skin/UICommonSetup.swift deleted file mode 100644 index 6fcabc3..0000000 --- a/crush/Crush/Src/Components/Skin/UICommonSetup.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// UICommonSetup.swift -// Crush -// -// Created by Leon on 2025/9/14. -// - -class UICommonSetup{ - - static func setupCLStyle(){ - //Textfield等输入框的统一光标颜色 - UITextField.appearance().tintColor = .c.cpn - UITextView.appearance().tintColor = .c.cpn - - // UISwitch的填满色 - UISwitch.appearance().onTintColor = .c.cpn//UIColor.systemBlue - //UISwitch.appearance().thumbTintColor = UIColor.white - } -} diff --git a/crush/Crush/Src/Components/Skin/token_global.json b/crush/Crush/Src/Components/Skin/token_global.json deleted file mode 100644 index 4c3d163..0000000 --- a/crush/Crush/Src/Components/Skin/token_global.json +++ /dev/null @@ -1,195 +0,0 @@ -{ -"glo.color.orange.0":"#FFECDE", -"glo.color.orange.10":"#FFD7B8", -"glo.color.orange.20":"#FFBF8F", -"glo.color.orange.30":"#FFA264", -"glo.color.orange.40":"#FD8239", -"glo.color.orange.50":"#F25E0F", -"glo.color.orange.60":"#D04500", -"glo.color.orange.70":"#A83400", -"glo.color.orange.80":"#7B2300", -"glo.color.orange.90":"#4D1400", -"glo.color.yellow.0":"#FFF8DE", -"glo.color.yellow.10":"#FFEFB3", -"glo.color.yellow.20":"#FFE386", -"glo.color.yellow.30":"#FCD258", -"glo.color.yellow.40":"#F3BC2A", -"glo.color.yellow.50":"#E6A100", -"glo.color.yellow.60":"#C78800", -"glo.color.yellow.70":"#A26B00", -"glo.color.yellow.80":"#784D00", -"glo.color.yellow.90":"#4D2F00", -"glo.color.grass.0":"#F8FFDE", -"glo.color.grass.10":"#EDFCB8", -"glo.color.grass.20":"#E0F68F", -"glo.color.grass.30":"#CFED67", -"glo.color.grass.40":"#BAE041", -"glo.color.grass.50":"#A0CD1E", -"glo.color.grass.60":"#82B500", -"glo.color.grass.70":"#689600", -"glo.color.grass.80":"#4B7200", -"glo.color.grass.90":"#304D00", -"glo.color.green.0":"#DEFFE7", -"glo.color.green.10":"#B9FCCD", -"glo.color.green.20":"#94F7B1", -"glo.color.green.30":"#6FEE96", -"glo.color.green.40":"#4AE27B", -"glo.color.green.50":"#28D061", -"glo.color.green.60":"#0BB84A", -"glo.color.green.70":"#00983C", -"glo.color.green.80":"#007331", -"glo.color.green.90":"#004D22", -"glo.color.mint.0":"#DEFFF8", -"glo.color.mint.10":"#B6FBED", -"glo.color.mint.20":"#8DF3E2", -"glo.color.mint.30":"#65E9D5", -"glo.color.mint.40":"#3FDAC4", -"glo.color.mint.50":"#1DC7B0", -"glo.color.mint.60":"#00AD96", -"glo.color.mint.70":"#009182", -"glo.color.mint.80":"#006F67", -"glo.color.mint.90":"#004D49", -"glo.color.sky.0":"#DEECFF", -"glo.color.sky.10":"#B5D2FD", -"glo.color.sky.20":"#8CB5F9", -"glo.color.sky.30":"#6296F2", -"glo.color.sky.40":"#3A76E6", -"glo.color.sky.50":"#1E58D2", -"glo.color.sky.60":"#063BB8", -"glo.color.sky.70":"#002A98", -"glo.color.sky.80":"#001E73", -"glo.color.sky.90":"#00134D", -"glo.color.blue.0":"#DEE0FF", -"glo.color.blue.10":"#BCBEFF", -"glo.color.blue.20":"#9797FF", -"glo.color.blue.30":"#7370FF", -"glo.color.blue.40":"#4E48FF", -"glo.color.blue.50":"#3126E6", -"glo.color.blue.60":"#180AC7", -"glo.color.blue.70":"#0F00A2", -"glo.color.blue.80":"#0D0078", -"glo.color.blue.90":"#09004D", -"glo.color.violet.0":"#E4DEFF", -"glo.color.violet.10":"#C7B7FD", -"glo.color.violet.20":"#AA90F9", -"glo.color.violet.30":"#8D68F2", -"glo.color.violet.40":"#7B47FF", -"glo.color.violet.50":"#5923D2", -"glo.color.violet.60":"#4309B8", -"glo.color.violet.70":"#340098", -"glo.color.violet.80":"#290073", -"glo.color.violet.90":"#1C004D", -"glo.color.purple.0":"#FBDEFF", -"glo.color.purple.10":"#F2B7FD", -"glo.color.purple.20":"#E690F9", -"glo.color.purple.30":"#D668F2", -"glo.color.purple.40":"#C241E6", -"glo.color.purple.50":"#A823D2", -"glo.color.purple.60":"#8A09B8", -"glo.color.purple.70":"#6E0098", -"glo.color.purple.80":"#520073", -"glo.color.purple.90":"#36004D", -"glo.color.magenta.0":"#FBDEFF", -"glo.color.magenta.10":"#FDB6D3", -"glo.color.magenta.20":"#F98DBC", -"glo.color.magenta.30":"#F264A4", -"glo.color.magenta.40":"#E63C8B", -"glo.color.magenta.50":"#D21F77", -"glo.color.magenta.60":"#B80761", -"glo.color.magenta.70":"#980050", -"glo.color.magenta.80":"#73003E", -"glo.color.magenta.90":"#4D002A", -"glo.color.red.0":"#FFDEDE", -"glo.color.red.10":"#FFBCBC", -"glo.color.red.20":"#FF9696", -"glo.color.red.30":"#F97372", -"glo.color.red.40":"#EF4E4D", -"glo.color.red.50":"#E12A2A", -"glo.color.red.60":"#C2110E", -"glo.color.red.70":"#A00700", -"glo.color.red.80":"#770800", -"glo.color.red.90":"#4D0600", -"glo.color.grey.0":"#E8E4EB", -"glo.color.grey.10":"#D4D0D8", -"glo.color.grey.20":"#AAA3B1", -"glo.color.grey.30":"#958E9E", -"glo.color.grey.40":"#847D8B", -"glo.color.grey.50":"#706A78", -"glo.color.grey.60":"#5C5565", -"glo.color.grey.70":"#484151", -"glo.color.grey.80":"#352E3E", -"glo.color.grey.90":"#282233", -"glo.color.grey.100":"#211A2B", -"glo.color.white":"#FFFFFF", -"glo.color.black":"#000000", -"glo.transparent.t0":"0", -"glo.transparent.t2":"0.02", -"glo.transparent.t4":"0.04", -"glo.transparent.t6":"0.06", -"glo.transparent.t8":"0.08", -"glo.transparent.t12":"0.12", -"glo.transparent.t15":"0.15", -"glo.transparent.t20":"0.2", -"glo.transparent.t25":"0.25", -"glo.transparent.t30":"0.3", -"glo.transparent.t45":"0.45", -"glo.transparent.t60":"0.60", -"glo.transparent.t65":"0.65", -"glo.transparent.t85":"0.85", -"glo.transparent.t100":"1.0", -"glo.deg.ltr":"LTR", -"glo.deg.ttb":"TTB", -"glo.deg.lttrb":"LTTRB", -"glo.font.family.sys":"Poppins", -"glo.font.family.sys.italic": "Poppins-Italic", -"glo.font.family.numDisplay":"DIN Alternate", -"glo.font.family.num":"JetBrains Mono", -"glo.font.family.display":"Oleo Script Swash Caps", -"glo.font.size.64":"64", -"glo.font.size.48":"48", -"glo.font.size.36":"36", -"glo.font.size.24":"24", -"glo.font.size.20":"20", -"glo.font.size.18":"18", -"glo.font.size.16":"16", -"glo.font.size.14":"14", -"glo.font.size.12":"12", -"glo.font.weight.regular":"400", -"glo.font.weight.medium":"500", -"glo.font.weight.semibold":"600", -"glo.font.weight.bold":"700", -"glo.font.lineheight.size64":"16", -"glo.font.lineheight.size48":"8", -"glo.font.lineheight.size36":"12", -"glo.font.lineheight.size24":"4", -"glo.font.lineheight.size20":"4", -"glo.font.lineheight.size18":"6", -"glo.font.lineheight.size16":"8", -"glo.font.lineheight.size14":"6", -"glo.font.lineheight.size12":"8", -"glo.font.lineheight.size0":"0", -"glo.radio.1.1":"1", -"glo.radio.4.3":"4/3", -"glo.radio.3.2":"3/2", -"glo.radio.2.1":"2", -"glo.radio.16.9":"16/9", -"glo.radius.4":"4", -"glo.radius.8":"8", -"glo.radius.12":"12", -"glo.radius.16":"16", -"glo.radius.round":"0.5", -"glo.border.half":"0.5", -"glo.border.1":"1", -"glo.border.2":"2", -"glo.border.4":"4", -"glo.spacing.4":"4", -"glo.spacing.8":"8", -"glo.spacing.12":"12", -"glo.spacing.16":"16", -"glo.spacing.24":"24", -"glo.spacing.32":"32", -"glo.spacing.48":"48", -"glo.spacing.64":"64", -"glo.spacing.80":"80", -"glo.spacing.128":"128" -} diff --git a/crush/Crush/Src/Components/Skin/token_sys_normal.json b/crush/Crush/Src/Components/Skin/token_sys_normal.json deleted file mode 100644 index c892ecc..0000000 --- a/crush/Crush/Src/Components/Skin/token_sys_normal.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "color.primary.normal": "$glo.color.magenta.50", - "color.primary.hover": "$glo.color.magenta.40", - "color.primary.press": "$glo.color.magenta.60", - "color.primary.disabled": "$color.surface.nest.disabled", - "color.primary.variant.normal": "$glo.color.magenta.40", - "color.primary.variant.hover": "$glo.color.magenta.30", - "color.primary.variant.press": "$glo.color.magenta.50", - "color.primary.variant.disabled": "$color.surface.nest.disabled", - "color.primary.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.magenta.30,$glo.color.purple.40", - "color.primary.gradient.hover": "@GRA:$glo.deg.ltr,$glo.color.magenta.20,$glo.color.purple.30", - "color.primary.gradient.press": "@GRA:$glo.deg.ltr,$glo.color.magenta.40,$glo.color.purple.50", - "color.primary.gradient.disabled": "$color.surface.nest.disabled", - "color.important.normal": "$glo.color.red.50", - "color.important.hover": "$glo.color.red.40", - "color.important.press": "$glo.color.red.60", - "color.important.disabled": "$color.surface.nest.disabled", - "color.important.variant.normal": "$glo.color.red.40", - "color.important.variant.hover": "$glo.color.red.30", - "color.important.variant.press": "$glo.color.red.50", - "color.important.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25", - "color.important.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.orange.50,$glo.color.red.50", - "color.important.gradient.hover": "@GRA:$glo.deg.ltr,$glo.color.orange.40,$glo.color.red.40", - "color.important.gradient.press": "@GRA:$glo.deg.ltr,$glo.color.orange.60,$glo.color.red.60", - "color.important.onpic.normal": "$glo.color.red.50,$glo.transparent.t85", - "color.positive.normal": "$glo.color.mint.60", - "color.positive.hover": "$glo.color.mint.50", - "color.positive.press": "$glo.color.mint.70", - "color.positive.disabled": "$color.surface.nest.disabled", - "color.positive.variant.normal": "$glo.color.mint.40", - "color.positive.variant.hover": "$glo.color.mint.30", - "color.positive.variant.press": "$glo.color.mint.50", - "color.positive.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25", - "color.positive.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.green.40,$glo.color.mint.60", - "color.positive.onpic.normal": "$glo.color.mint.60,$glo.transparent.t85", - "color.warning.normal": "$glo.color.orange.50", - "color.warning.hover": "$glo.color.orange.40", - "color.warning.press": "$glo.color.orange.60", - "color.warning.disabled": "$color.surface.nest.disabled", - "color.warning.variant.normal": "$glo.color.orange.40", - "color.warning.variant.hover": "$glo.color.orange.30", - "color.warning.variant.press": "$glo.color.orange.50", - "color.warning.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25", - "color.warning.gradient.normal": "@GRA:$glo.deg.ltr,$glo.color.yellow.40,$glo.color.orange.50", - "color.warning.onpic.normal": "$glo.color.orange.50,$glo.transparent.t85", - "color.emphasis.normal": "$glo.color.blue.40", - "color.emphasis.hover": "$glo.color.blue.30", - "color.emphasis.press": "$glo.color.blue.50", - "color.emphasis.disabled": "$color.surface.nest.disabled", - "color.emphasis.variant.normal": "$glo.color.blue.30", - "color.emphasis.variant.hover": "$glo.color.blue.20", - "color.emphasis.variant.press": "$glo.color.blue.40", - "color.emphasis.variant.disabled": "$glo.color.blue.10,$glo.transparent.t25", - "color.emphasis.grandient.normal": "@GRA:$glo.deg.ltr,$glo.color.sky.30,$glo.color.blue.40", - "color.emphasis.grandient.hover": "@GRA:$glo.deg.ltr,$glo.color.sky.20,$glo.color.blue.30", - "color.emphasis.grandient.press": "@GRA:$glo.deg.ltr,$glo.color.sky.40,$glo.color.blue.50", - "color.emphasis.grandient.disabled": "$color.surface.nest.disabled", - "color.emphasis.onpic.normal": "$glo.color.blue.40,$glo.transparent.t85", - "color.background.default": "$glo.color.grey.100", - "color.background.specialmap": "$glo.color.grey.100", - "color.background.district": "$glo.color.black,$glo.transparent.t30", - "color.surface.base.normal": "$glo.color.grey.80", - "color.surface.base.hover": "$glo.color.grey.70", - "color.surface.base.press": "$glo.color.grey.90", - "color.surface.base.specialmap.normal": "$glo.color.grey.80", - "color.surface.base.specialmap.hover": "$glo.color.grey.70", - "color.surface.base.specialmap.press": "$glo.color.grey.90,$glo.transparent.t30", - "color.surface.base.specialmap.disabled": "$glo.color.white,$glo.transparent.t8", - "color.surface.float.normal": "$glo.color.grey.70", - "color.surface.float.hover": "$glo.color.grey.60", - "color.surface.float.press": "$glo.color.grey.80", - "color.surface.top.normal": "$glo.color.black,$glo.transparent.t65", - "color.surface.top.hover": "$glo.color.black,$glo.transparent.t45", - "color.surface.top.press": "$glo.color.black,$glo.transparent.t85", - "color.surface.top.disabled": "$glo.color.black,$glo.transparent.t30", - "color.surface.district.normal": "$glo.color.purple.0,$glo.transparent.t4", - "color.surface.district.hover": "$glo.color.purple.0,$glo.transparent.t12", - "color.surface.district.press": "$glo.color.black,$glo.transparent.t25", - "color.surface.district.disabled": "$glo.color.black,$glo.transparent.t25", - "color.surface.nest.normal": "$glo.color.purple.0,$glo.transparent.t8", - "color.surface.nest.hover": "$glo.color.purple.0,$glo.transparent.t12", - "color.surface.nest.press": "$glo.color.purple.0,$glo.transparent.t4", - "color.surface.nest.disabled": "$glo.color.purple.0,$glo.transparent.t4", - "color.surface.element.normal": "$color.surface.nest.normal", - "color.surface.element.hover": "$color.surface.nest.hover", - "color.surface.element.press": "$color.surface.nest.press", - "color.surface.element.disabled": "$color.surface.nest.disabled", - "color.surface.element.dark.normal": "$glo.color.black,$glo.transparent.t65", - "color.surface.element.dark.hover": "$glo.color.black,$glo.transparent.t45", - "color.surface.element.dark.press": "$glo.color.black,$glo.transparent.t85", - "color.surface.element.dark.disabled": "$glo.color.black,$glo.transparent.t45", - "color.surface.element.light.normal": "$glo.color.white,$glo.transparent.t15", - "color.surface.element.light.hover": "$glo.color.white,$glo.transparent.t25", - "color.surface.element.light.press": "$glo.color.white,$glo.transparent.t8", - "color.surface.element.light.disabled": "$glo.color.white,$glo.transparent.t8", - "color.surface.white.normal": "$glo.color.white", - "color.surface.white.hover": "$glo.color.white,$glo.transparent.t85", - "color.surface.white.press": "$glo.color.white,$glo.transparent.t65", - "color.surface.white.disabled": "$glo.color.white,$glo.transparent.t45", - "color.surface.black.normal":"$glo.color.black", - "color.outline.normal": "$glo.color.purple.0,$glo.transparent.t20", - "color.outline.hover": "$glo.color.purple.0,$glo.transparent.t30", - "color.outline.press": "$glo.color.purple.0,$glo.transparent.t8", - "color.outline.disabled": "$color.surface.element.disabled", - "color.overlay.primary": "$glo.color.violet.30,$glo.transparent.t30", - "color.overlay.gradient": "@GRA:$glo.deg.ttb,$glo.color.black&$glo.transparent.t0,$glo.color.black&$glo.transparent.t100", - "color.overlay.dark": "$glo.color.black,$glo.transparent.t65", - "color.overlay.background": "@GRA:$glo.deg.ttb,$color.background.default&$glo.transparent.t0,$color.background.default&$glo.transparent.t100", - "color.overlay.base": "", - "color.context.vip.normal":"@GRA:$glo.deg.ltr,$glo.color.red.20,$glo.color.violet.20,$glo.color.mint.20", - "color.txt.primary.normal": "$glo.color.white", - "color.txt.primary.hover": "$glo.color.magenta.30", - "color.txt.primary.press": "$glo.color.magenta.40", - "color.txt.primary.disabled": "$color.txt.disabled", - "color.txt.primary.specialmap.normal": "$glo.color.white", - "color.txt.primary.specialmap.hover": "$glo.color.white,$glo.transparent.t85", - "color.txt.primary.specialmap.press": "$glo.color.white,$glo.transparent.t65", - "color.txt.primary.specialmap.disabled": "$glo.color.white,$glo.transparent.t45", - "color.txt.secondary.normal": "$glo.color.grey.30", - "color.txt.secondary.hover": "$glo.color.magenta.30", - "color.txt.secondary.press": "$glo.color.magenta.40", - "color.txt.secondary.disabled": "$color.txt.disabled", - "color.txt.tertiary.normal": "$glo.color.grey.40", - "color.txt.grass": "$glo.color.grass.40", - "color.txt.disabled": "$glo.color.grey.50", - "txt.display": "$glo.font.family.sys,$glo.font.size.48,$glo.font.weight.bold,$glo.font.lineheight.size48", - "txt.display.xl": "$glo.font.family.display,$glo.font.size.64,$glo.font.weight.regular,$glo.font.lineheight.size64", - "txt.display.l": "$glo.font.family.display,$glo.font.size.36,$glo.font.weight.regular,$glo.font.lineheight.size36", - "txt.display.m": "$glo.font.family.display,$glo.font.size.24,$glo.font.weight.regular,$glo.font.lineheight.size24", - "txt.display.s": "$glo.font.family.display,$glo.font.size.16,$glo.font.weight.regular,$glo.font.lineheight.size16", - "txt.headline.l": "$glo.font.family.sys,$glo.font.size.36,$glo.font.weight.bold,$glo.font.lineheight.size36", - "txt.headline.m": "$glo.font.family.sys,$glo.font.size.24,$glo.font.weight.bold,$glo.font.lineheight.size24", - "txt.headline.s": "$glo.font.family.sys,$glo.font.size.20,$glo.font.weight.bold,$glo.font.lineheight.size20", - "txt.title.l": "$glo.font.family.sys,$glo.font.size.20,$glo.font.weight.semibold,$glo.font.lineheight.size24", - "txt.title.m": "$glo.font.family.sys,$glo.font.size.18,$glo.font.weight.semibold,$glo.font.lineheight.size18", - "txt.title.s": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.semibold,$glo.font.lineheight.size16", - "txt.bodySemibold.l": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.semibold,$glo.font.lineheight.size16", - "txt.bodySemibold.m": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.semibold,$glo.font.lineheight.size14", - "txt.bodySemibold.s": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.semibold,$glo.font.lineheight.size12", - "txt.body.l": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.regular,$glo.font.lineheight.size16", - "txt.body.m": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.regular,$glo.font.lineheight.size14", - "txt.body.italic.m": "$glo.font.family.sys.italic,$glo.font.size.14,$glo.font.weight.regular,$glo.font.lineheight.size14", - "txt.body.s": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.regular,$glo.font.lineheight.size12", - "txt.label.l": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.medium,$glo.font.lineheight.size16", - "txt.label.m": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.medium,$glo.font.lineheight.size14", - "txt.label.s": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.medium,$glo.font.lineheight.size12", - "txt.numDisplay.xl": "$glo.font.family.numDisplay,$glo.font.size.48,$glo.font.weight.bold,$glo.font.lineheight.size48", - "txt.numDisplay.l": "$glo.font.family.numDisplay,$glo.font.size.36,$glo.font.weight.bold,$glo.font.lineheight.size36", - "txt.numDisplay.m": "$glo.font.family.numDisplay,$glo.font.size.24,$glo.font.weight.bold,$glo.font.lineheight.size24", - "txt.numDisplay.s": "$glo.font.family.numDisplay,$glo.font.size.20,$glo.font.weight.bold,$glo.font.lineheight.size20", - "txt.numMonotype.xl": "$glo.font.family.sys,$glo.font.size.20,$glo.font.weight.bold,$glo.font.lineheight.size20", - "txt.numMonotype.l": "$glo.font.family.sys,$glo.font.size.18,$glo.font.weight.bold,$glo.font.lineheight.size18", - "txt.numMonotype.m": "$glo.font.family.sys,$glo.font.size.16,$glo.font.weight.bold,$glo.font.lineheight.size16", - "txt.numMonotype.s": "$glo.font.family.sys,$glo.font.size.14,$glo.font.weight.medium,$glo.font.lineheight.size14", - "txt.numMonotype.xs": "$glo.font.family.sys,$glo.font.size.12,$glo.font.weight.medium,$glo.font.lineheight.size12", - "shadow.s": "@SHA:$glo.color.black,$glo.transparent.t15,0&0,4", - "shadow.m": "@SHA:$glo.color.black,$glo.transparent.t15,0&0,8", - "shadow.l": "@SHA:$glo.color.black,$glo.transparent.t15,0&0,16", - "radius.xs": "$glo.radius.4", - "radius.s": "$glo.radius.8", - "radius.m": "$glo.radius.12", - "radius.l": "$glo.radius.16", - "radius.xl" : "$glo.radius.20", - "radius.xxl" : "$glo.radius.24", - "radius.round": "$glo.radius.round", - "radius.pill": "$glo.radius.round", - "border.divider": "$glo.border.half", - "border.s": "$glo.border.1", - "border.m": "$glo.border.2", - "border.l": "$glo.border.4" -} diff --git a/crush/Crush/Src/Components/UI/Alert/Alert.swift b/crush/Crush/Src/Components/UI/Alert/Alert.swift deleted file mode 100644 index 27b6f40..0000000 --- a/crush/Crush/Src/Components/UI/Alert/Alert.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// File.swift -// Crush -// -// Created by Leon on 2025/7/14. -// - -import UIKit - -class Alert: EGNewNormalAlert { -} - -class AlertAction: EGNewAlertAction { -} - -class Sheet: EGActionSheet { -} - -class SheetAction: EGActionSheetAction { -} diff --git a/crush/Crush/Src/Components/UI/Alert/AlertCL.swift b/crush/Crush/Src/Components/UI/Alert/AlertCL.swift deleted file mode 100644 index 5f566b2..0000000 --- a/crush/Crush/Src/Components/UI/Alert/AlertCL.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// AlertCL.swift -// Crush -// -// Created by Leon on 2025/8/4. -// - -extension Alert{ - class func showAIRoleCreateSuccessAlert(cancelAction: @escaping () -> Void, confirmAction: @escaping () -> Void){ - let content = "Please confirm that the virtual character is your original or fan creation and does not infringe on others' images, IP or other rights." - let alert = Alert(title: "", text: content, image: UIImage(named: "logo_ai_role")!) - alert.titleBGview.isHidden = true -// alert.imageView.snp.updateConstraints { make in -// make.size.equalTo(CGSize(100, 100)) -// make.top.equalToSuperview().offset(24) -// } - alert.masTopToImageBgForImageView?.update(offset: 24) - let confirm = AlertAction(title: "Confirm", actionStyle: .confirm, block: confirmAction) - let cancel = AlertAction(title: "Cancel", actionStyle: .cancel, block: cancelAction) - alert.addAction(confirm) - alert.addAction(cancel) - alert.show() - alert.layoutIfNeeded() - } -} diff --git a/crush/Crush/Src/Components/UI/Alert/EGNewBaseAlert.swift b/crush/Crush/Src/Components/UI/Alert/EGNewBaseAlert.swift deleted file mode 100644 index 675525a..0000000 --- a/crush/Crush/Src/Components/UI/Alert/EGNewBaseAlert.swift +++ /dev/null @@ -1,372 +0,0 @@ -// -// EGNewBaseAlert.swift -// Crush -// -// Created by Leon on 2025/8/3. -// - -import UIKit -import SnapKit - -// MARK: - EGNewAlertAction - -enum EGNewAlertActionStyle: Int { - case `default` - case cancel - case confirm - case disabled - case inputSave - case destructive -} - -class EGNewAlertAction : Equatable { - var title: String - var attributedTitle: NSAttributedString? - var actionStyle: EGNewAlertActionStyle - var autoDismiss: Bool - var actionBlock: (() -> Void)? - - init(title: String, actionStyle: EGNewAlertActionStyle = .default, autoDismiss: Bool = true, block: (() -> Void)? = nil) { - self.title = title - self.attributedTitle = nil - self.actionStyle = actionStyle - self.autoDismiss = autoDismiss - self.actionBlock = block - } - - // 新增:使用 NSAttributedString 的初始化方法 - init(attributedTitle: NSAttributedString, actionStyle: EGNewAlertActionStyle = .default, autoDismiss: Bool = true, block: (() -> Void)? = nil) { - self.title = "" // 当使用富文本时,title 为空 - self.attributedTitle = attributedTitle - self.actionStyle = actionStyle - self.autoDismiss = autoDismiss - self.actionBlock = block - } - - static func action(title: String, block: (() -> Void)? = nil) -> EGNewAlertAction { - return EGNewAlertAction(title: title, block: block) - } - - static func action(title: String, actionStyle: EGNewAlertActionStyle, block: (() -> Void)? = nil) -> EGNewAlertAction { - return EGNewAlertAction(title: title, actionStyle: actionStyle, block: block) - } - - static func action(title: String, actionStyle: EGNewAlertActionStyle, autoDismiss: Bool, block: (() -> Void)? = nil) -> EGNewAlertAction { - return EGNewAlertAction(title: title, actionStyle: actionStyle, autoDismiss: autoDismiss, block: block) - } - - // 新增:使用 NSAttributedString 的静态方法 - static func action(attributedTitle: NSAttributedString, block: (() -> Void)? = nil) -> EGNewAlertAction { - return EGNewAlertAction(attributedTitle: attributedTitle, block: block) - } - - static func action(attributedTitle: NSAttributedString, actionStyle: EGNewAlertActionStyle, block: (() -> Void)? = nil) -> EGNewAlertAction { - return EGNewAlertAction(attributedTitle: attributedTitle, actionStyle: actionStyle, block: block) - } - - static func action(attributedTitle: NSAttributedString, actionStyle: EGNewAlertActionStyle, autoDismiss: Bool, block: (() -> Void)? = nil) -> EGNewAlertAction { - return EGNewAlertAction(attributedTitle: attributedTitle, actionStyle: actionStyle, autoDismiss: autoDismiss, block: block) - } - - // MARK: - Equatable - static func == (lhs: EGNewAlertAction, rhs: EGNewAlertAction) -> Bool { - return lhs === rhs // 🔥 Using identity comparison; adjust if unique identifier exists - } -} - -// MARK: - EGNewBaseAlert - -enum EGNewAlertPriority: Int { - case `default` = 0 - case update = 10 - case forceUpdate = 100 -} - -class EGNewBaseAlert: UIView { - // MARK: - Properties - private(set) var backgroundView: UIView! - private(set) var containerView: UIView! - private(set) var textContentView: UIView! - private(set) var buttonContainer: UIView! - private(set) var buttons: [StyleButton] = [] - private(set) var actions: [EGNewAlertAction] = [] - var priority: EGNewAlertPriority = .default - private var maxActionCount: Int = 2 - - var containerWidth: CGFloat { - return UIScreen.main.bounds.width - containerMarginLR() * 2 - } - - // MARK: - Initialization - override init(frame: CGRect) { - super.init(frame: frame) - baseDataInit() - baseUIInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - baseDataInit() - baseUIInit() - } - - private func baseDataInit() { - maxActionCount = 2 - buttons = [] - actions = [] - } - - private func baseUIInit() { - backgroundView = UIView().then { - addSubview($0) - $0.backgroundColor = .c.cbs - $0.alpha = 0 - $0.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - } - - containerView = UIView().then { - addSubview($0) - $0.backgroundColor = .c.csbn // 🔥 Assuming EPS_Color_Surface_base_normal is a light color - $0.layer.cornerRadius = containerCornerRadius() - $0.clipsToBounds = true - $0.alpha = 0 - let width = UIScreen.main.bounds.width * 0.8 - $0.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.equalTo(width) - } - } - - textContentView = UIView().then { - containerView.addSubview($0) - $0.backgroundColor = .clear - $0.setContentHuggingPriority(.defaultLow, for: .vertical) - $0.snp.makeConstraints { make in - make.leading.trailing.top.equalToSuperview() - make.height.greaterThanOrEqualTo(textContentMinHeight()) - } - } - - buttonContainer = UIView().then { - containerView.addSubview($0) - $0.backgroundColor = .clear - $0.snp.makeConstraints { make in - make.top.equalTo(textContentView.snp.bottom) - make.leading.trailing.bottom.equalToSuperview() - } - } - } - - // MARK: - Layout Methods (Overridable) - func containerMarginLR() -> CGFloat { - return 40 - } - - func textContentMaxHeight() -> CGFloat { - return UIScreen.main.bounds.height * 0.65 - } - - func textContentMinHeight() -> CGFloat { - return 100 - } - - func containerCornerRadius() -> CGFloat { - return 16 - } - - func containerAlphaDuration() -> CGFloat { - return 0.35 - } - - func backgroundAlphaValue() -> CGFloat { - return 0.65 - } - - func alertInitRatio() -> CGFloat { - return 0.6 - } - - // MARK: - Actions - func addAction(_ action: EGNewAlertAction) { - addAction(action, propertySetup: nil) - } - - func addAction(_ action: EGNewAlertAction, propertySetup: ((UIButton) -> Void)?) { - guard actions.count < maxActionCount else { return } - - let button = StyleButton(type: .custom) - button.addTarget(self, action: #selector(baseButtonPressed(_:)), for: .touchUpInside) - - // 支持富文本和普通文本 - if let attributedTitle = action.attributedTitle { - button.setAttributedTitle(attributedTitle, for: .normal) - } else { - button.setTitle(action.title, for: .normal) - } - - button.tag = actions.count - button.titleLabel?.font = .t.tbsl// EPS_Txt_BodySemibold_l is a bold font - reloadButton(button, style: action.actionStyle) - - if let setup = propertySetup { - setup(button) - } - - actions.append(action) - buttons.append(button) - } - - func reloadAction(_ action: EGNewAlertAction, title: String) { - guard let index = actions.firstIndex(of: action) else { - assertionFailure("Action not found") - return - } - let button = buttons[index] - button.setTitle(title, for: .normal) - reloadButton(button, style: action.actionStyle) - } - - // 新增:支持富文本的 reloadAction 方法 - func reloadAction(_ action: EGNewAlertAction, attributedTitle: NSAttributedString) { - guard let index = actions.firstIndex(of: action) else { - assertionFailure("Action not found") - return - } - let button = buttons[index] - button.setAttributedTitle(attributedTitle, for: .normal) - reloadButton(button, style: action.actionStyle) - } - - private func reloadButton(_ button: StyleButton, style: EGNewAlertActionStyle) { - button.isEnabled = true - switch style { - case .default, .cancel: - button.tertiary(size: .large) - case .confirm,.inputSave: - button.primary(size: .large) - case .disabled: - button.tertiary(size: .large) - button.isEnabled = false - case .destructive: - button.defaultDestructive(size: .large) - } - } - - // MARK: - Events - @objc private func baseButtonPressed(_ button: UIButton) { - endEditing(true) - let action = actions[button.tag] - if action.autoDismiss { - dismiss() - } - action.actionBlock?() - } - - func alertDidShow() { - // Overridable by subclasses - } - - // MARK: - Animation - func showWithAnimation() { - containerView.transform = CGAffineTransform(scaleX: alertInitRatio(), y: alertInitRatio()) - UIView.animate(withDuration: containerAlphaDuration(), delay: 0, options: .curveEaseOut, animations: { - self.backgroundView.alpha = self.backgroundAlphaValue() - self.containerView.alpha = 1 - self.containerView.transform = .identity - }) { _ in - self.alertDidShow() - } - } - - func dismissWithAnimation() { - UIView.animate(withDuration: containerAlphaDuration(), delay: 0, options: .curveEaseIn, animations: { - self.containerView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) - self.backgroundView.alpha = 0 - self.containerView.alpha = 0 - }) { _ in - self.removeFromSuperview() - } - } - - // MARK: - Display - func show() { - // ⚠️ Before is windows's first - //guard let window = UIApplication.shared.windows.first else { return } - - // 第二种方式: -// var window: UIWindow! -// for per in UIApplication.shared.windows.reversed(){ -// if per.isHidden == false{ -// window = per -// break -// } -// } - - // 第3种方式: 会过滤一些 - var window: UIWindow! - window = UIWindow.getTopDisplayWindow()! - - if window == nil{ - assert(false, "alert window not found") - } - - window.endEditing(true) - - // 隐藏已有的Alert(覆盖显示) - for subview in window.subviews { - if let oldAlert = subview as? EGNewBaseAlert { - if oldAlert.priority != .default && oldAlert.priority.rawValue >= priority.rawValue { - return - } else { - oldAlert.dismiss() - } - } - } - - window.addSubview(self) - self.frame = window.bounds - showWithAnimation() - - Hud.hideIndicator() - } - - func dismiss() { - dismissWithAnimation() - } - - static func existingAlert() -> Bool { - return existingAlertObj() != nil - } - - static func existingAlertObj() -> EGNewBaseAlert? { - //guard let window = UIApplication.shared.windows.first else { return nil } - guard let window = UIWindow.getTopDisplayWindow() else { return nil } - return window.subviews.first(where: { $0 is EGNewBaseAlert }) as? EGNewBaseAlert - } - - static func hideAllAlert() { - guard let window = UIApplication.shared.windows.first else { return } - for subview in window.subviews { - if let alert = subview as? EGNewBaseAlert { - alert.isHidden = true - alert.removeFromSuperview() - } - } - } - - func setupMaxActionCount(_ count: Int) { - maxActionCount = count - } -} - -// MARK: - Then Protocol for Chaining -protocol Then {} -extension Then where Self: AnyObject { - func then(_ block: (Self) -> Void) -> Self { - block(self) - return self - } -} -extension UIView: Then {} diff --git a/crush/Crush/Src/Components/UI/Alert/EGNewNormalAlert.swift b/crush/Crush/Src/Components/UI/Alert/EGNewNormalAlert.swift deleted file mode 100644 index 3a40561..0000000 --- a/crush/Crush/Src/Components/UI/Alert/EGNewNormalAlert.swift +++ /dev/null @@ -1,434 +0,0 @@ -// -// EGNewNormalAlert.swift -// Crush -// -// Created by Leon on 2025/8/3. -// - -import SnapKit -import UIKit - -class EGNewNormalAlert: EGNewBaseAlert { - // MARK: - Properties - - var showCloseButton: Bool = false - private(set) var contentStackView: UIStackView! - private(set) var imageBGview: UIView! - private(set) var imageView: UIImageView! - - /// 隐藏titleBGView才能隐藏titleLabel - private(set) var titleBGview: UIView! - private(set) var titleLabel: UILabel! - - private(set) var textBGview: UIView! - private(set) var textView: UITextView! - - private(set) var optionContainer: UIView! - private(set) var btnsPartStackView: UIStackView! - private var closeButton: EPIconGhostButton! - private(set) var masTopToImageBgForImageView: Constraint? - private(set) var masBottomToImageBgForImageView: Constraint? - private var image: UIImage? - var counting: Int = 0 - var operatingAction: EGNewAlertAction? - var timerEndAction: (() -> Void)? - - private let kMarginLRText: CGFloat = 24 - private let kMarginTBButton: CGFloat = 10 - private let kMarginContent: CGFloat = 24 - - var bgBlurImageNode:Bool = false - - lazy var bgImageView = { - let v = AutoRatioImageView() - v.setContentHuggingPriority(UILayoutPriority(240), for: .horizontal) - v.setContentHuggingPriority(UILayoutPriority(240), for: .vertical) - v.setContentCompressionResistancePriority(UILayoutPriority(740), for: .horizontal) - v.setContentCompressionResistancePriority(UILayoutPriority(740), for: .vertical) - containerView.insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview().priority(.medium) - } - v.isHidden = true - return v - }() - - lazy var effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - v.alpha = 1 - containerView.insertSubview(v, aboveSubview: bgImageView) - v.snp.makeConstraints { make in -// make.edges.equalToSuperview() - make.edges.equalTo(bgImageView) - } - return v - }() - - override func layoutSubviews() { - super.layoutSubviews() - if bgBlurImageNode{ - bgImageView.frame = containerView.bounds - } - } - - // MARK: - Initialization - - init(title: String, text: String) { - super.init(frame: .zero) - uiInit() - titleLabel.text = title - textView.text = text - reloadLayout() - } - - init(title: String, lineSpaceText text: String) { - super.init(frame: .zero) - uiInit() - titleLabel.text = title -// let paragraphStyle = NSMutableParagraphStyle().then { -// $0.paragraphSpacing = 6 -// $0.lineSpacing = 3 -// } - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.paragraphSpacing = 6 - paragraphStyle.lineSpacing = 3 - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 16), // 🔥 Assuming EPF.bm is a body font - .foregroundColor: UIColor.black, // 🔥 Assuming EPC.tpn is a primary text color - .paragraphStyle: paragraphStyle, - ] - textView.attributedText = NSAttributedString(string: text, attributes: attributes) - reloadLayout() - } - - init(title: String, attributeText text: NSAttributedString) { - super.init(frame: .zero) - uiInit() - titleLabel.text = title - textView.attributedText = text - reloadLayout() - } - - init(title: String, text: String, image: UIImage) { - super.init(frame: .zero) - self.image = image - uiInit() - titleLabel.text = title - textView.text = text - reloadLayout() - } - - init(title: String, attributeText text: NSAttributedString, image: UIImage?) { - super.init(frame: .zero) - self.image = image - uiInit() - titleLabel.text = title - textView.attributedText = text - reloadLayout() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - uiInit() - reloadLayout() - } - - private func uiInit() { - contentStackView = UIStackView().then { - textContentView.addSubview($0) - $0.spacing = kMarginContent - $0.axis = .vertical - $0.alignment = .center - $0.snp.makeConstraints { make in - make.top.equalToSuperview().offset(image != nil ? 0 : 32) - make.bottom.leading.trailing.equalToSuperview() - } - } - - imageBGview = UIView().then { - contentStackView.addArrangedSubview($0) - $0.isUserInteractionEnabled = false - $0.isHidden = true - $0.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - } - - imageView = UIImageView().then { - imageBGview.addSubview($0) - $0.snp.makeConstraints { make in - self.masTopToImageBgForImageView = make.top.equalToSuperview().offset(0).constraint - self.masBottomToImageBgForImageView = make.bottom.equalToSuperview().offset(0).constraint - make.center.equalToSuperview() - make.size.equalTo(CGSize.zero) - } - } - - titleBGview = UIView().then { - contentStackView.addArrangedSubview($0) - $0.isUserInteractionEnabled = false - $0.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(kMarginLRText) - make.trailing.equalToSuperview().offset(-kMarginLRText) - } - } - - titleLabel = UILabel().then { - titleBGview.addSubview($0) - $0.font = .t.ttm - $0.textColor = .c.ctpn - $0.textAlignment = .center - $0.numberOfLines = 2 - $0.setContentHuggingPriority(.required, for: .vertical) - $0.setContentCompressionResistancePriority(.required, for: .vertical) - $0.snp.makeConstraints { make in - make.centerX.top.bottom.equalToSuperview() - make.leading.greaterThanOrEqualToSuperview() - make.trailing.lessThanOrEqualToSuperview() - } - } - - textBGview = UIView().then { - contentStackView.addArrangedSubview($0) - $0.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(kMarginLRText) - make.trailing.equalToSuperview().offset(-kMarginLRText) - } - } - - textView = UITextView().then { - textBGview.addSubview($0) - $0.backgroundColor = .clear - $0.font = .t.tbm - $0.textColor = .c.ctpn - $0.textAlignment = .center - $0.isScrollEnabled = false - $0.isEditable = false - $0.isSelectable = false - $0.showsVerticalScrollIndicator = false - $0.showsHorizontalScrollIndicator = false - $0.indicatorStyle = .white - $0.setContentHuggingPriority(.defaultLow, for: .vertical) - $0.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - $0.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.height.equalTo(0) - } - } - - optionContainer = UIView().then { - contentStackView.addArrangedSubview($0) - $0.isHidden = true - $0.snp.makeConstraints { make in - make.leading.equalTo(kMarginLRText) - make.trailing.equalTo(-kMarginLRText) - } - } - - closeButton = EPIconGhostButton(radius: .none, iconSize: .small, iconCode: .delete).then { - textContentView.addSubview($0) - $0.isHidden = true - $0.addTarget(self, action: #selector(closeAction), for: .touchUpInside) - $0.snp.makeConstraints { make in - make.top.equalToSuperview().offset(6) - make.trailing.equalToSuperview().offset(-10) - make.size.equalTo(CGSize(width: 32, height: 32)) - } - } - - btnsPartStackView = UIStackView().then { - buttonContainer.addSubview($0) - $0.spacing = 24 - $0.distribution = .equalSpacing - $0.axis = .vertical - $0.alignment = .center - $0.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.bottom.equalToSuperview().offset(-24) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - } - } - - func reloadLayout() { - let textWidth = containerWidth - (kMarginLRText * 2) - var imageHeight: CGFloat = 0 - var imageWidth: CGFloat = 0 - var height: CGFloat = 0 - - if let image = image { - imageBGview.isHidden = false - imageView.image = image - imageHeight = image.size.height - imageWidth = image.size.width - if imageWidth > textWidth { - let newWidth = containerWidth - imageHeight = imageHeight * (newWidth / imageWidth) - imageWidth = newWidth - } - imageView.snp.updateConstraints { make in - make.size.equalTo(CGSize(width: imageWidth, height: imageHeight)) - } - } else { - height = kMarginContent + kMarginTBButton - } - - let fitSize = CGSize(width: textWidth, height: .greatestFiniteMagnitude) - let titleHeight = titleLabel.sizeThatFits(fitSize).height + 0.6 - let textHeight = textView.sizeThatFits(fitSize).height + 0.6 - - if image != nil { - height += imageHeight + 12 - height += kMarginContent - } - if !titleLabel.text!.isEmpty { - height += titleHeight - height += kMarginContent - } - height += kMarginTBButton - - let minHeight = textContentMinHeight() - let maxHeight = textContentMaxHeight() - var textViewHeight = textHeight - - textView.isScrollEnabled = false - if height + textHeight > maxHeight { - textViewHeight = maxHeight - height - textView.isScrollEnabled = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in - self?.textView.flashScrollIndicators() - } - } else if height + textHeight < minHeight { - textViewHeight = minHeight - height - } - - textView.snp.updateConstraints { make in - make.height.equalTo(textViewHeight) - } - } - - func layoutButtons() { - guard !actions.isEmpty else { return } - let alertWidth = containerWidth - kMarginLRText * 2 - let buttonWidth = btnsPartStackView.axis == .horizontal - ? (alertWidth - CGFloat(actions.count - 1) * 8) / CGFloat(actions.count) - : alertWidth - - for button in buttons { - btnsPartStackView.addArrangedSubview(button) - button.snp.makeConstraints { make in - make.width.equalTo(buttonWidth) - } - } - } - - @objc private func closeAction() { - endEditing(true) - dismiss() - } - - deinit { - print("🌪️ EGNewNormalAlert dealloc") - } - - // MARK: - Override - - override func show() { - if showCloseButton { - titleLabel.snp.updateConstraints { make in - make.leading.greaterThanOrEqualToSuperview().offset(16) // 🔥 Assuming pageLR is 16 - make.trailing.lessThanOrEqualToSuperview().offset(-16) - } - closeButton.isHidden = false - } - layoutButtons() - super.show() - } - - // MARK: - Public Methods - - func setupButtonAxis(_ buttonAxis: NSLayoutConstraint.Axis) { - btnsPartStackView.axis = buttonAxis - } - - func setupContentAlignment(_ textAlignment: NSTextAlignment) { - textView.textAlignment = textAlignment - } - - func setupButtonsSpacing(_ spacing: CGFloat) { - btnsPartStackView.spacing = spacing - } - - func setupContentStackSpacing(_ spacing: CGFloat) { - contentStackView.spacing = spacing - } - - func setupButtonsStackLayoutTop(_ margin: CGFloat) { - btnsPartStackView.snp.updateConstraints { make in - make.top.equalToSuperview().offset(margin) - } - } - - func setupOptionContainerInnerView(_ view: UIView?) { - guard let view = view else { return } - optionContainer.isHidden = false - optionContainer.addSubview(view) - view.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - } - - func isTextViewScrollEnable() -> Bool { - return textView.contentSize.height > textView.bounds.size.height + 10 - } - - func refreshTextViewText(_ textObj: Any) { - if let text = textObj as? String { - textView.text = text - } else if let attributedText = textObj as? NSAttributedString { - textView.attributedText = attributedText - } else { - assertionFailure("Invalid text object") - } - } - - func addTopButtonForTextView(_ block: (() -> Void)?) { - let button = UIButton(type: .custom) - textBGview.addSubview(button) - button.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - button.addTarget(self, action: #selector(executeBlock(_:)), for: .touchUpInside) - objc_setAssociatedObject(button, &AssociatedKeys.block, block, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - - @objc private func executeBlock(_ sender: UIButton) { - let block = objc_getAssociatedObject(sender, &AssociatedKeys.block) as? (() -> Void) - block?() - } - - func setupBottomBlurImage(image: UIImage?){ - bgBlurImageNode = true - bgImageView.image = image - bgImageView.isHidden = false - effectView.isHidden = false - layoutIfNeeded() - } - - func setupBottomBlurImageUrl(url: String?) { -// guard let urlObj = URL(string: url) else { -// return -// } - guard let urlString = url else{return} - - bgImageView.setImage(with: urlString.urlValue) - bgImageView.isHidden = false - effectView.isHidden = false - layoutIfNeeded() - } -} - -private struct AssociatedKeys { - static var block = "block" -} diff --git a/crush/Crush/Src/Components/UI/BaseView/AvatarView.swift b/crush/Crush/Src/Components/UI/BaseView/AvatarView.swift deleted file mode 100644 index 41c9c62..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/AvatarView.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// AvatarView.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -class AvatarView: UIView { - // MARK: - Properties - var avatar: CLAnimatedImage! - var cameraCircle: UIView! - var cameraIcon: UIImageView! - var indicator: IndicatorView! - var topButton: UIButton! - - var tapAction: (() -> Void)? - - // MARK: - Initialization - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - private func setupViews() { - avatar = { - let imageView = CLAnimatedImage(frame: .zero) - imageView.backgroundColor = .c.csen - addSubview(imageView) - imageView.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.centerY.equalToSuperview() - //make.size.equalTo(CGSize(width: 80, height: 80)) - make.edges.equalToSuperview() - } - return imageView - }() - - cameraCircle = { - let view = UIView() - view.backgroundColor = .c.csfn - view.layer.cornerRadius = 16 - view.layer.masksToBounds = true - view.layer.borderColor = UIColor.c.cbd.cgColor - view.layer.borderWidth = 2 - addSubview(view) - view.snp.makeConstraints { make in - make.trailing.equalTo(avatar.snp.trailing).offset(4) - make.bottom.equalTo(avatar.snp.bottom).offset(-2) - make.size.equalTo(CGSize(width: 32, height: 32)) - } - view.isHidden = true - return view - }() - - cameraIcon = { - let imageView = UIImageView() - imageView.image = MWIconFont.image(fromIcon: .iconCameraFill, size: CGSizeMake(16, 16), color: .white) - imageView.contentMode = .scaleAspectFit - cameraCircle.addSubview(imageView) - imageView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 16, height: 16)) - } - return imageView - }() - - indicator = { - let v = IndicatorView(color: .text) - addSubview(v) - v.snp.makeConstraints { make in - make.center.equalTo(avatar) - } - v.isHidden = true - return v - }() - - topButton = { - let button = UIButton() - addSubview(button) - button.snp.makeConstraints { make in - make.size.equalTo(CGSizeMake(90, 90)) - make.center.equalTo(avatar) - } - button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) - return button - }() - } - - // MARK: - Actions - @objc private func buttonTapped() { - tapAction?() - } - - // MARK: - Public Methods - func bindImage(img: UIImage?, showUploading: Bool = false) { - avatar.image = img - - if showUploading { - indicator.isHidden = false - indicator.startIndicator() - } else { - indicator.isHidden = true - indicator.stopIndicator() - } - } - - func bindImageUrl(imgUrl: String?) { - avatar.loadImage(imgUrl) - indicator.stopIndicator() - indicator.isHidden = true - } - - func setTapAction(_ action: @escaping () -> Void) { - tapAction = action - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/BadgeView.swift b/crush/Crush/Src/Components/UI/BaseView/BadgeView.swift deleted file mode 100755 index 1e7068c..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/BadgeView.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// BadgeView.swift -// LegendTeam -// -// Created by 梁博 on 16/12/21. -// - -import UIKit - -class BadgeView: UIView { - /// 是否显示为小红点,默认false - public var onlyShowPoint = false - /// 是否超过99 只显示... 默认false - public var showMax99 = false - /// 显示红点数值,设置为0会自动隐藏 - public var badgeValue = 0 { - didSet { - reloadBadge() - } - } - - /// 显示为红点的时候的宽高 默认 8 - public var pointWidth: CGFloat = 8 - /// 字体大小 默认 - public var badgeFont: UIFont = .t.tnmxs - - private var leftPadding: CGFloat = 4 - private var rightPadding: CGFloat = 4 - private let defaultBgColor = UIColor.c.cin - private let defaultTextColor = UIColor.white - private let contentLabel = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - private func setupUI() { - isUserInteractionEnabled = false - backgroundColor = defaultBgColor - layer.masksToBounds = true - - snp.makeConstraints { make in - make.width.greaterThanOrEqualTo(snp.height) - } - - addSubview(contentLabel) - contentLabel.textColor = defaultTextColor - contentLabel.font = badgeFont - contentLabel.textAlignment = .center - contentLabel.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - } - - private func reloadBadge() { - isHidden = false - contentLabel.isHidden = false - - if badgeValue == 0 { - isHidden = true - return - } - - if onlyShowPoint { - contentLabel.isHidden = true - contentLabel.snp.remakeConstraints { make in - make.edges.equalToSuperview() - make.size.equalTo(CGSize(width: pointWidth, height: pointWidth)) - } - return - } - - if showMax99 && badgeValue > 99 { - contentLabel.text = "···" - } else { - contentLabel.text = String(badgeValue) - } - contentLabel.snp.remakeConstraints { make in - make.leading.equalTo(self).offset(leftPadding) - make.trailing.equalTo(self).offset(-rightPadding) - make.width.greaterThanOrEqualTo(6) - make.top.bottom.equalTo(self) - } - } - - override func layoutSubviews() { - super.layoutSubviews() - if bounds.size.width > 0 { - layer.cornerRadius = bounds.size.height * 0.5 - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/CLContainer.swift b/crush/Crush/Src/Components/UI/BaseView/CLContainer.swift deleted file mode 100644 index 3087a65..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/CLContainer.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// CLContainer.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -import UIKit - -class CLContainer : UIView{ - /// ⚠️请不要在init(frame: CGRect)中使用导航栏 - var navigationView: NavigationView? - -// convenience init(navi: NavigationView){ -// navigationView = navi -// self.init(frame: .zero) -// } - - override init(frame: CGRect) { - super.init(frame: frame) - } - -// convenience init(navigationView: NavigationView) { -// self.init(frame: .zero) // 调用上面的 designated initializer -// self.navigationView = navigationView -// } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/CLDatePicker.swift b/crush/Crush/Src/Components/UI/BaseView/CLDatePicker.swift deleted file mode 100644 index ed67e2a..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/CLDatePicker.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// CLDatePicker.swift -// Crush -// -// Created by Leon on 2025/7/19. -// -import UIKit - -class CLDatePicker : UIDatePicker{ - override func layoutSubviews() { - super.layoutSubviews() - backgroundColor = .clear - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/CLImageViews.swift b/crush/Crush/Src/Components/UI/BaseView/CLImageViews.swift deleted file mode 100644 index 4e09340..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/CLImageViews.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// CLImageViews.swift -// Crush -// -// Created by Leon on 2025/7/20. -// -import UIKit -import SnapKit - -class CLAnimatedImage: UIImageView { - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - override init(image: UIImage?) { - super.init(image: image) - setupViews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupViews() - } - - private func setupViews() { - contentMode = .scaleAspectFill - } - - override func layoutSubviews() { - super.layoutSubviews() - layer.cornerRadius = bounds.size.height * 0.5 - layer.masksToBounds = true - } -} - -class CLImageView: UIImageView { - convenience init(){ - self.init(frame: .zero) - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - override init(image: UIImage?) { - super.init(image: image) - setupViews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupViews() - } - - private func setupViews() { - contentMode = .scaleAspectFill - } -} - -/// 约束时,请不要给定底部的约束 -class AutoRatioImageView: CLImageView { - - // 用来保存宽高比约束 - private var aspectRatioConstraint: Constraint? - - convenience init() { - self.init(frame: .zero) - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupView() - } - - private func setupView() { - contentMode = .scaleAspectFill - clipsToBounds = true // 裁剪多余部分 - - self.snp.makeConstraints { make in - make.height.equalTo(self.snp.width).priority(.low) - } - } - - /// 设置本地图片 - func setImage(_ image: UIImage?) { - self.image = image - updateAspectRatio(for: image?.size) - } - - /// 设置网络图片 - func setImage(with url: URL?, placeholder: UIImage? = nil) { - self.kf.setImage(with: url, placeholder: placeholder, completionHandler: { [weak self] result in - switch result { - case .success(let value): - self?.updateAspectRatio(for: value.image.size) - case .failure: - self?.updateAspectRatio(for: placeholder?.size) - } - }) - } - - - /// 更新宽高比约束 - private func updateAspectRatio(for size: CGSize?) { - guard let size = size, size.width > 0 else { return } - let ratio = size.height / size.width - - // 先移除旧的约束 - aspectRatioConstraint?.deactivate() - - // 高度 = 宽度 * 比例 - self.snp.makeConstraints { make in - aspectRatioConstraint = make.height.equalTo(self.snp.width).multipliedBy(ratio).constraint - } - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/CLLine.swift b/crush/Crush/Src/Components/UI/BaseView/CLLine.swift deleted file mode 100644 index 8054fb8..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/CLLine.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CLLine.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit - -class CLLine: UIView { - convenience init() { - self.init(frame: .zero) - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = .c.con - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/CLPageControl.swift b/crush/Crush/Src/Components/UI/BaseView/CLPageControl.swift deleted file mode 100644 index e234da4..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/CLPageControl.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// CLPageControl.swift -// Crush -// -// Created by Leon on 2025/9/4. -// - -import UIKit - -class CLPageControl: UIPageControl { - convenience init() { - self.init(frame: .zero) - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - numberOfPages = 0 - currentPage = 0 - pageIndicatorTintColor = .white.withAlphaComponent(0.45) - currentPageIndicatorTintColor = .white - hidesForSinglePage = true - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/FlowLayoutContainer.swift b/crush/Crush/Src/Components/UI/BaseView/FlowLayoutContainer.swift deleted file mode 100644 index 19ee45f..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/FlowLayoutContainer.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// FlowContainer.swift -// Crush -// -// Created by Leon on 2025/7/19. -// - -import UIKit - -class FlowAutoLayoutContainer: UIView { - // MARK: - Properties - - /// Item 之间的水平间距 - var itemSpacing: CGFloat = 8.0 { - didSet { setNeedsLayout() } - } - - /// 行之间的垂直间距 - var lineSpacing: CGFloat = 8.0 { - didSet { setNeedsLayout() } - } - - /// 存储子视图 - private var arrangedSubviews: [UIView] = [] - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - - private func setup() { - // 设置默认背景色 - backgroundColor = .clear - } - - // MARK: - Public Methods - - /// 添加单个子视图 - func addArrangedSubview(_ view: UIView) { - arrangedSubviews.append(view) - addSubview(view) - setNeedsLayout() - } - - /// 添加多个子视图 - func addArrangedSubviews(_ views: [UIView]) { - arrangedSubviews.append(contentsOf: views) - views.forEach { addSubview($0) } - setNeedsLayout() - } - - /// 清空所有子视图 - func removeAllArrangedSubviews() { - arrangedSubviews.forEach { $0.removeFromSuperview() } - arrangedSubviews.removeAll() - setNeedsLayout() - } - - // MARK: - Layout - - override func layoutSubviews() { - super.layoutSubviews() - - // 清空现有约束 - arrangedSubviews.forEach { $0.snp.removeConstraints() } - - // 当前布局参数 - let containerWidth = bounds.width - var currentX: CGFloat = 0 - var currentY: CGFloat = 0 - var currentRowHeight: CGFloat = 0 - var currentRowViews: [UIView] = [] // 当前行的所有views - var previousView: UIView? - - // 遍历子视图进行布局 - for view in arrangedSubviews { - view.translatesAutoresizingMaskIntoConstraints = false - let viewSize = view.intrinsicContentSize == .zero ? view.bounds.size : view.intrinsicContentSize - - // 检查是否需要换行 - if currentX + viewSize.width > containerWidth && !currentRowViews.isEmpty { - // 布局当前行(底部对齐) - layoutRow(views: currentRowViews, rowHeight: currentRowHeight, startY: currentY, previousView: &previousView) - - // 重置参数开始新行 - currentX = 0 - currentY += currentRowHeight + lineSpacing - currentRowHeight = 0 - currentRowViews.removeAll() - } - - // 更新当前行信息 - currentRowViews.append(view) - currentX += viewSize.width + itemSpacing - currentRowHeight = max(currentRowHeight, viewSize.height) - } - - // 布局最后一行 - if currentRowViews.isEmpty == false { // has views in lastViews - layoutRow(views: currentRowViews, rowHeight: currentRowHeight, startY: currentY, previousView: &previousView) - if let last = currentRowViews.last { - //dlog("💤currentY:\(currentY)") - last.snp.makeConstraints { make in - make.top.equalToSuperview().offset(currentY) - } - } - invalidateIntrinsicContentSize() // 🔥重要 - } - - //dlog("💤currentRowHeight \(currentRowHeight)") - bounds = CGRect(x: 0, y: 0, width: intrinsicContentSize.width, height: intrinsicContentSize.height) - //dlog("💤currentRowHeight:\(currentRowHeight) intrinsicContentSize:\(intrinsicContentSize) sizeHeight:\(bounds.size.height)") - } - - /// 布局一行中的视图,底部对齐 - private func layoutRow(views: [UIView], rowHeight: CGFloat, startY: CGFloat, previousView: inout UIView?) { - for (index, view) in views.enumerated() { - view.snp.makeConstraints { make in - // 底部对齐:bottom = startY + rowHeight - make.bottom.equalTo(snp.top).offset(startY + rowHeight) - - if index == 0 { - // 每行第一个视图,靠容器左边 - make.leading.equalToSuperview() - } else if let prev = previousView { - // 非第一个视图,紧接前一个视图 - make.leading.equalTo(prev.snp.trailing).offset(itemSpacing) - } - - // 高度基于 intrinsicContentSize - if view.intrinsicContentSize.height != UIView.noIntrinsicMetric { - make.height.equalTo(view.intrinsicContentSize.height) - } - } - previousView = view - } - } - - /// 计算容器所需的高度 - override var intrinsicContentSize: CGSize { -// layoutIfNeeded() -// -// var totalHeight: CGFloat = 0 -// var currentX: CGFloat = 0 -// var currentRowHeight: CGFloat = 0 -// -// for view in arrangedSubviews { -// let viewSize = view.intrinsicContentSize == .zero ? view.bounds.size : view.intrinsicContentSize -// -// if currentX + viewSize.width > bounds.width && currentX > 0 { -// totalHeight += currentRowHeight + lineSpacing -// currentX = 0 -// currentRowHeight = 0 -// } -// -// currentX += viewSize.width + itemSpacing -// currentRowHeight = max(currentRowHeight, viewSize.height) -// } -// -// // 加上最后一行的行高 -// if currentRowHeight > 0 { -// totalHeight += currentRowHeight -// } -// -// dlog("💤Totalheight: \(totalHeight)") -// return CGSize(width: bounds.width, height: totalHeight) - - layoutIfNeeded() - - if subviews.count > 0{ - let view = subviews.last! - // dlog("💤 last:\(view)") - let height = max(view.frame.maxY, 30) - return CGSize(width: bounds.width, height: height) - } - return CGSize(width: bounds.width, height: 30) - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/GradientViews/GradientBorderView.swift b/crush/Crush/Src/Components/UI/BaseView/GradientViews/GradientBorderView.swift deleted file mode 100644 index 2284ea6..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/GradientViews/GradientBorderView.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// GradientBorderView.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -/// 渐变边框容器 -class GradientBorderView: UIView { - private var gradientLayer: CAGradientLayer? - private var colors: [UIColor] = [] - private var gradientType: GradientType = .leftToRight // Assuming GradientType is an enum; default to horizontal - private var innerBackgroundColor: UIColor = .clear - - // MARK: - Initialization - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - fatalError("init(nibName:bundle:) has not been implemented") - } - - override init(frame: CGRect) { - super.init(frame: frame) - } - - convenience init(colors: [UIColor], gradientType: GradientType) { - self.init(frame: .zero) - self.backgroundColor = .clear - self.colors = colors - self.gradientType = gradientType - self.gBorderWidth = 1.0 // Assuming DP(1) maps to 1.0 point - self.innerBackgroundColor = .clear - } - - // MARK: - Lifecycle - - override func layoutSubviews() { - super.layoutSubviews() - - gradientLayer?.removeFromSuperlayer() - - if !colors.isEmpty { - let cgColors = colors.map { $0.cgColor } - addGradientBorder(with: cgColors, width: gBorderWidth) - } - } - - // MARK: - Public Properties - - var gBorderWidth: CGFloat = 0{ - didSet { - setNeedsLayout() - } - } - - // MARK: - Private Methods - - private func addGradientBorder(with colors: [CGColor], width: CGFloat) { - // 创建一个渐变层 - let gradientLayer = CAGradientLayer() - - gradientLayer.frame = bounds - gradientLayer.colors = colors - gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) - gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) - let step = 1.0 / Double(colors.count) - var locations: [NSNumber] = [] - var locationValue: Double = 0 - - for _ in 0.. 0 { - shapeLayer.path = UIBezierPath(roundedRect: insetRect, cornerRadius: layer.cornerRadius).cgPath - } else { - shapeLayer.path = UIBezierPath(rect: insetRect).cgPath - } - shapeLayer.lineWidth = width - shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.strokeColor = UIColor.black.cgColor - - // 将形状层设置为渐变层的遮罩 - gradientLayer.mask = shapeLayer - - // 将渐变层添加到视图的图层上 - layer.addSublayer(gradientLayer) - self.gradientLayer = gradientLayer - } -} - -// MARK: - Unavailable Initializers - -//extension GradientBorderView { -// @available(*, unavailable) -// class func new() -> Self { -// fatalError("new() is unavailable") -// } -// -// @available(*, unavailable) -// override convenience init() { -// fatalError("init() is unavailable") -// } -// -// @available(*, unavailable) -// override convenience init(frame: CGRect) { -// fatalError("init(frame:) is unavailable") -// } -// -// @available(*, unavailable) -// required convenience init?(coder: NSCoder) { -// fatalError("init(coder:) is unavailable") -// } -// -// @available(*, unavailable) -// convenience init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { -// fatalError("init(nibName:bundle:) is unavailable") -// } -//} - -// MARK: - Assumptions and Unknowns - - -// Unknown: 'hex_clear' is assumed to be UIColor.clear based on context (clear background) -// Unknown: 'DP(1)' is assumed to map to 1.0 point as a design pixel unit; adjust if a different scaling is intended -// Unknown: The full definition of GradientType is not provided; a default horizontal gradient is assumed diff --git a/crush/Crush/Src/Components/UI/BaseView/GradientViews/GradientView.swift b/crush/Crush/Src/Components/UI/BaseView/GradientViews/GradientView.swift deleted file mode 100644 index cbb743b..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/GradientViews/GradientView.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// GradientView.swift -// Crush -// -// Created by Leon on 2025/7/15. -// - -import Foundation -import UIKit - -enum GradientType { - case topToBottom - case leftToRight - case upleftToLowright - case uprightToLowleft -} - -/// 支持2~4个颜色,内置默认的分割 -class GradientView: UIView { - private var gradientLayer: CAGradientLayer? - var colors: [UIColor] = [] - var gradientType: GradientType = .leftToRight - var specialStyleForVip: Bool = false - var imBgOverlayMode : Bool = false - - // MARK: - Initialization - init(colors: [UIColor], gradientType: GradientType) { - super.init(frame: .zero) - self.backgroundColor = .clear - self.colors = colors - self.gradientType = gradientType - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Class Method - static func vipGradientView(with size: CGSize) -> GradientView { - let view = GradientView(colors: [ - UIColor(hex: 0x8CB5F9, alpha: 1.0), - UIColor(hex: 0x7B47FF, alpha: 1.0), - UIColor(hex: 0xD668F2, alpha: 1.0) - ], gradientType: .leftToRight) - - view.frame = CGRect(origin: .zero, size: size) - view.specialStyleForVip = true - return view - } - - // MARK: - Public Methods - func modifyColors(_ colors: [UIColor], gradientType: GradientType) { - self.colors = colors - self.gradientType = gradientType - setNeedsLayout() - } - - // MARK: - Private Methods - func layoutVipSubViews() { - // Implement VIP-specific layout if needed - } - - // MARK: - Layout - override func layoutSubviews() { - super.layoutSubviews() - - // Remove existing gradient layer - gradientLayer?.removeFromSuperlayer() - gradientLayer = nil - - // Check bounds and colors - guard bounds.size.width > 0, colors.count >= 2 else { - // assertionFailure("GradientView requires at least 2 colors and non-zero width") - return - } - - // Configure gradient points - var start: CGPoint = .zero - var end: CGPoint = .zero - - switch gradientType { - case .topToBottom: - start = CGPoint(x: 0.0, y: 0.0) - end = CGPoint(x: 0.0, y: 1.0) - case .leftToRight: - start = CGPoint(x: 0.0, y: 0.0) - end = CGPoint(x: 1.0, y: 0.0) - case .upleftToLowright: - start = CGPoint(x: 0.0, y: 0.0) - end = CGPoint(x: 1.0, y: 1.0) - case .uprightToLowleft: - start = CGPoint(x: 1.0, y: 0.0) - end = CGPoint(x: 0.0, y: 1.0) - } - - // Create and configure gradient layer - let gradientLayer = CAGradientLayer() - gradientLayer.frame = bounds - gradientLayer.colors = colors.map { $0.cgColor } - gradientLayer.startPoint = start - gradientLayer.endPoint = end - - // Set gradient locations - var locations:[NSNumber] = [0, 1.0] - if(specialStyleForVip){ - locations = [0, 0.6, 1] as [NSNumber] - }else if colors.count == 3{ - locations = [0, 0.5, 1] as [NSNumber] - }else if imBgOverlayMode{ - locations = [0, 0.2,0.5, 1] as [NSNumber] - }else if colors.count == 4{ - locations = [0, 0.33, 0.66, 1] as [NSNumber] - } - gradientLayer.locations = locations - // specialStyleForVip ? [0, 0.6, 1] : (colors.count >= 3 ? [0, 0.5, 1] : [0, 1]) - - layer.insertSublayer(gradientLayer, at: 0) - self.gradientLayer = gradientLayer - } -} - -// MARK: - UIColor Extension -extension UIColor { - convenience init(hex: UInt32, alpha: CGFloat) { - let red = CGFloat((hex >> 16) & 0xFF) / 255.0 - let green = CGFloat((hex >> 8) & 0xFF) / 255.0 - let blue = CGFloat(hex & 0xFF) / 255.0 - self.init(red: red, green: green, blue: blue, alpha: alpha) - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/LTScrollContainer.swift b/crush/Crush/Src/Components/UI/BaseView/LTScrollContainer.swift deleted file mode 100755 index 33e6da5..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/LTScrollContainer.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// LTScrollContainer.swift -// LegendTeam -// -// Created by dong on 2022/5/5. -// - -import Combine -import UIKit - -/// Default: leading,trailing is 0, stack's alignment is leading -class LTScrollContainer: UIView { - public var scrollView: UIScrollView! - var container: UIView! - public var stack: UIStackView! - -// var listViewDidScrollCallback: ((UIScrollView) -> Void)? - private var cancellables = Set() - - var stackEdge:UIEdgeInsets?{ - didSet{ - stack.snp.updateConstraints { make in - make.edges.equalToSuperview().inset(stackEdge ?? .zero) - } - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = .clear - - scrollView = UIScrollView() - scrollView.contentInsetAdjustmentBehavior = .never - addSubview(scrollView) - scrollView.snp.makeConstraints { make in - make.left.right.equalToSuperview() -// make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) -// make.bottom.equalToSuperview().offset(-64 - UIWindow.safeAreaBottom) - make.top.bottom.equalToSuperview() - } - - container = UIView() - scrollView.addSubview(container) - container.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.width.equalToSuperview() - } - - stack = UIStackView() - stack.axis = .vertical - stack.alignment = .leading - container.addSubview(stack) - stack.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0)) - } - - scrollContainerEvent() - } - - private func scrollContainerEvent() { -// scrollView.publisher(for: \.contentOffset) -// .sink {[weak self] offset in -// //print("Scroll offset is now \(offset)") -// if let scroll = self?.scrollView { -// self?.listViewDidScrollCallback?(scroll) -// } -// } -// .store(in: &cancellables) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/SelectiveDeliveryEventsView.swift b/crush/Crush/Src/Components/UI/BaseView/SelectiveDeliveryEventsView.swift deleted file mode 100644 index 3bafe4a..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/SelectiveDeliveryEventsView.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SelectiveDeliveryEventsView.swift -// Crush -// -// Created by Leon on 2025/7/27. -// - -import UIKit - -/// 选择性传递View,常用做容器,只上方的一些控制事件可响应。(✅允许Control事件) -class SelectiveDeliveryEventsView: UIView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - // 1. 如果 view 是隐藏的或不交互,返回 nil - if self.isHidden || self.alpha < 0.01 || !self.isUserInteractionEnabled { - return nil - } - - // 2. 遍历子视图,看看是否有 UIControl 响应点击 - for subview in subviews.reversed() { - let convertedPoint = subview.convert(point, from: self) - if let hitView = subview.hitTest(convertedPoint, with: event) { - if hitView is UIControl { - return hitView // 点到了控件,返回控件自身,停止传递 - } - } - } - - // 3. 没有控件响应,返回 nil 让事件传递给父视图 - return nil - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/TagsFlowLayout.swift b/crush/Crush/Src/Components/UI/BaseView/TagsFlowLayout.swift deleted file mode 100644 index 9d424f2..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/TagsFlowLayout.swift +++ /dev/null @@ -1,131 +0,0 @@ -// MIT License -// -// Copyright (c) 2022 Alexandr Sibirtsev -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import UIKit - -public extension TagsFlowLayout { - - enum LayoutAlignment: Int { - case left - case center - case right - } -} - -public class TagsFlowLayout: UICollectionViewFlowLayout { - - let alignment: LayoutAlignment - - //MARK: - Init Methods - - required init(alignment: LayoutAlignment = .left, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) { - self.alignment = alignment - super.init() - - self.minimumInteritemSpacing = minimumInteritemSpacing - self.minimumLineSpacing = minimumLineSpacing - self.sectionInset = sectionInset - - self.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - self.sectionInsetReference = SectionInsetReference.fromLayoutMargins - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let superArray = super.layoutAttributesForElements(in: rect) else { return nil } - guard let attributes = NSArray(array: superArray, copyItems: true) as? [UICollectionViewLayoutAttributes] else { return nil } - // Constants - let leftPadding: CGFloat = 8 - let interItemSpacing = minimumInteritemSpacing - // Tracking values - var leftMargin: CGFloat = leftPadding // Modified to determine origin.x for each item - var maxY: CGFloat = -1.0 // Modified to determine origin.y for each item - var rowSizes: [[CGFloat]] = [] // Tracks the starting and ending x-values for the first and last item in the row - var currentRow: Int = 0 // Tracks the current row - attributes.forEach { layoutAttribute in - guard layoutAttribute.representedElementCategory == .cell else { - return - } - // Each layoutAttribute represents its own item - if layoutAttribute.frame.origin.y >= maxY { - // This layoutAttribute represents the left-most item in the row - leftMargin = leftPadding - // Register its origin.x in rowSizes for use later - if rowSizes.count == 0 { - // Add to first row - rowSizes = [[leftMargin, 0]] - } else { - // Append a new row - rowSizes.append([leftMargin, 0]) - currentRow += 1 - } - } - layoutAttribute.frame.origin.x = leftMargin - leftMargin += layoutAttribute.frame.width + interItemSpacing - maxY = max(layoutAttribute.frame.maxY, maxY) - // Add right-most x value for last item in the row - rowSizes[currentRow][1] = leftMargin - interItemSpacing - } - - guard alignment != .left && alignment != .right else { - return attributes - } - - // At this point, all cells are left aligned - // Reset tracking values and add extra left padding to center align entire row - leftMargin = leftPadding - maxY = -1.0 - currentRow = 0 - attributes.forEach { layoutAttribute in - // Each layoutAttribute is its own item - if layoutAttribute.frame.origin.y >= maxY { - // This layoutAttribute represents the left-most item in the row - leftMargin = leftPadding - // Need to bump it up by an appended margin - let rowWidth = rowSizes[currentRow][1] - rowSizes[currentRow][0] // last.x - first.x - let appendedMargin = (collectionView!.frame.width - leftPadding - rowWidth - leftPadding) / 2 - leftMargin += appendedMargin - currentRow += 1 - } - layoutAttribute.frame.origin.x = leftMargin - leftMargin += layoutAttribute.frame.width + interItemSpacing - maxY = max(layoutAttribute.frame.maxY, maxY) - } - - return attributes - } - - public override var flipsHorizontallyInOppositeLayoutDirection: Bool { - return true - } - - public override var developmentLayoutDirection: UIUserInterfaceLayoutDirection { - if alignment == .right { - return UIUserInterfaceLayoutDirection.rightToLeft - } else { - return UIUserInterfaceLayoutDirection.leftToRight - } - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/TitleAndSubTitleView.swift b/crush/Crush/Src/Components/UI/BaseView/TitleAndSubTitleView.swift deleted file mode 100644 index 951eaf2..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/TitleAndSubTitleView.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// TitleAndSubTitleView.swift -// Crush -// -// Created by Leon on 2025/7/28. -// - -class TitleAndSubTitleView: UIView{ - var titleLabel:UILabel! - var subLabel:LineSpaceLabel! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews(){ - titleLabel = { - let label = UILabel() - label.font = .t.tll - label.textColor = .text - label.textAlignment = .left - addSubview(label) - label.snp.makeConstraints { make in - make.left.equalToSuperview() - make.top.equalToSuperview() - make.right.equalToSuperview() - //make.height.equalTo(24) - } - return label - }() - subLabel = { - let label = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbl) - label.config(typo) - label.textColor = .text - label.textAlignment = .left - addSubview(label) - label.snp.makeConstraints { make in - make.left.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.right.equalToSuperview() - make.bottom.equalToSuperview() - } - return label - }() - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/TitleView.swift b/crush/Crush/Src/Components/UI/BaseView/TitleView.swift deleted file mode 100644 index 3d1d3f2..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/TitleView.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// TitleView.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -import UIKit - -class TitleView: UIView { - // MARK: - Properties - private var stackV: UIStackView - private(set)var titleLabel: LineSpaceLabel - private var subTitleLabel: LineSpaceLabel - private var stackSpacing: CGFloat = 16 - - var title: String? { - didSet { - titleLabel.text = title - } - } - - var subtitle: String? { - didSet { - if let subtitle = subtitle, !subtitle.isEmpty { - subTitleLabel.isHidden = false - subTitleLabel.text = subtitle - } else { - subTitleLabel.isHidden = true - subTitleLabel.text = "" - } - } - } - - var attributeSubTitle: NSAttributedString? { - didSet { - if let attributeSubTitle = attributeSubTitle, attributeSubTitle.length > 0 { - subTitleLabel.isHidden = false - subTitleLabel.text = "" - subTitleLabel.attributedText = attributeSubTitle - } else { - subTitleLabel.isHidden = true - subTitleLabel.attributedText = nil - } - } - } - - var titleHidden: Bool = false { - didSet { - titleLabel.isHidden = titleHidden - } - } - - /// Default: 16 - var optionInnerTopPadding: CGFloat = 16 { - didSet { - makeStackConstraint() - } - } - - /// Default: 16 - var optionInnerBottomPadding: CGFloat = 16 { - didSet { - makeStackConstraint() - } - } - - var optionInnerLRPadding: CGFloat = 24 { - didSet { - makeStackConstraint() - } - } - - var optionInnerTrailingAppend: CGFloat = 0 { - didSet { - makeStackConstraint() - } - } - - var alwaysDarkMode: Bool = false { - didSet { - if alwaysDarkMode { - titleLabel.textColor = .c.ctpn//EPSystemToken.darkColor(.txtPrimaryNormal) - subTitleLabel.textColor = .c.ctsn//EPSystemToken.darkColor(.txtSecondaryNormal) - } - } - } - - // MARK: - Initialization - override init(frame: CGRect) { - stackV = UIStackView() - titleLabel = LineSpaceLabel() - subTitleLabel = LineSpaceLabel() - - super.init(frame: frame) - - // Configure stack view - stackV.spacing = stackSpacing - stackV.axis = .vertical - addSubview(stackV) - stackV.snp.makeConstraints { make in - make.top.equalToSuperview().offset(optionInnerTopPadding) - make.bottom.equalToSuperview().offset(-optionInnerBottomPadding) - make.leading.equalToSuperview().offset(optionInnerLRPadding) - make.trailing.equalToSuperview().offset(-optionInnerLRPadding).priority(999) - } - - // Configure title label - let titleT = CLSystemToken.typography(token: .thm) - titleLabel.config(titleT) - titleLabel.numberOfLines = 0 - titleLabel.lineBreakMode = .byWordWrapping - stackV.addArrangedSubview(titleLabel) - - // Configure subtitle label - let subTitleT = CLSystemToken.typography(token: .tbm) - subTitleLabel.config(subTitleT) - subTitleLabel.numberOfLines = 0 - stackV.addArrangedSubview(subTitleLabel) - - // Theme configuration - self.titleLabel.textColor = .c.ctpn //EPSystemToken.color(.txtPrimaryNormal) - self.subTitleLabel.textColor = .c.ctsn //EPSystemToken.color(.txtSecondaryNormal) - - // Default state - subTitleLabel.isHidden = true - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Helper Methods - private func makeStackConstraint() { - stackV.snp.remakeConstraints { make in - make.top.equalToSuperview().offset(optionInnerTopPadding) - make.bottom.lessThanOrEqualToSuperview().offset(-optionInnerBottomPadding) - make.leading.equalToSuperview().offset(optionInnerLRPadding) - make.trailing.equalToSuperview().offset(-self.optionInnerLRPadding - self.optionInnerTrailingAppend) - } - } - - // MARK: - Public Methods -// func precalcHeight() -> CGFloat { -// preCalculateHeight() -// } - - func preCalculateHeight() -> CGFloat { - let topBottomPadding = optionInnerTopPadding + optionInnerBottomPadding - - // Calcualte way 1: height: 36 -// let titleT = CLSystemToken.typography(token: .thm) -// let lineSpacing = titleT.lineHeight * 0.5 -// let text = titleLabel.text ?? "" -// let labelWidth = UIScreen.main.bounds.width - optionInnerLRPadding * 2 - optionInnerTrailingAppend -// var titleHeight = heightForText(text, font: titleT.font!, width: labelWidth, lineSpacing: lineSpacing) - - // Calculate way 2: height: 33.6 - let titleHeight = titleLabel.isHidden ? 0 : titleLabel.sizeThatFits(CGSize(width: UIScreen.main.bounds.width - optionInnerLRPadding * 2 - optionInnerTrailingAppend, height: .greatestFiniteMagnitude)).height - // dlog("oldCalculateHeight: \(oldCalculateHeight), bouncingCalculate: \(titleHeight)") - - let subTitleHeight = subTitleLabel.isHidden ? 0 : subTitleLabel.sizeThatFits(CGSize(width: UIScreen.main.bounds.width - optionInnerLRPadding * 2 - optionInnerTrailingAppend, height: .greatestFiniteMagnitude)).height - - var total = topBottomPadding + titleHeight + subTitleHeight - if titleHeight > 0 && subTitleHeight > 0 { - total += stackSpacing - } - return ceil(total) - } - - func setupTitleSubtitleAlignment(_ alignment: NSTextAlignment) { - titleLabel.textAlignment = alignment - subTitleLabel.textAlignment = alignment - } - - // MARK: - Helper Methods - - func heightForText(_ text: String, font: UIFont, width: CGFloat, lineSpacing: CGFloat = 0) -> CGFloat { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineBreakMode = .byWordWrapping - paragraphStyle.lineSpacing = lineSpacing - - let attributes: [NSAttributedString.Key: Any] = [ - .font: font, - .paragraphStyle: paragraphStyle - ] - - let size = CGSize(width: width, height: .greatestFiniteMagnitude) - let rect = (text as NSString).boundingRect( - with: size, - options: [.usesLineFragmentOrigin, .usesFontLeading], - attributes: attributes, - context: nil - ) - return ceil(rect.height) - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/UploadImageView.swift b/crush/Crush/Src/Components/UI/BaseView/UploadImageView.swift deleted file mode 100644 index 1c441ee..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/UploadImageView.swift +++ /dev/null @@ -1,265 +0,0 @@ -// -// UploadImageView.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit -import SnapKit - -enum UploadImageState: UInt { - case `default` - case uploading - case success - case failed - case oversize - case yellow -} - -class UploadImageView: UIView { - // MARK: - Properties - let imageView: CLImageView - let errorLabel: UILabel - let deleteButton: EPIconTertiaryDarkButton - let indicatorView: UIActivityIndicatorView - - var state: UploadImageState = .default { - didSet { - dealUploadState(state) - } - } - - var notShowDeleteButton: Bool = false { - didSet { - deleteButton.alpha = notShowDeleteButton ? 0 : 1 - } - } - - var bgTapBlock: (() -> Void)? - var deleteTapBlock: (() -> Void)? - var retryTapBlock: (() -> Void)? - - private var bottomBottom: UIButton - private var bgButton: EPHighlightBorderButton - private var loadingView: UIView - private var indicatorLabel: UILabel - private var errorView: UIView - private var retryButton: UIButton - private var videoMode: Bool = false - - // MARK: - Initialization - override init(frame: CGRect) { - imageView = CLImageView(frame: .zero) - errorLabel = UILabel() - deleteButton = EPIconTertiaryDarkButton(radius: .round, iconSize: .small, iconCode: .delete) - indicatorView = UIActivityIndicatorView() - bottomBottom = UIButton(type: .custom) - bgButton = EPHighlightBorderButton(type: .custom) - loadingView = UIView() - indicatorLabel = UILabel() - errorView = UIView() - retryButton = UIButton() - - super.init(frame: frame) - setupUI() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - private func setupUI() { - backgroundColor = .c.csen//EPSystemToken.color(.surfaceElementNormal) - layer.cornerRadius = 8 - clipsToBounds = true - - // Bottom button - bottomBottom.backgroundColor = .clear - bottomBottom.addTarget(self, action: #selector(bgButtonTapAction), for: .touchUpInside) - addSubview(bottomBottom) - bottomBottom.snp.makeConstraints { make in - make.edges.equalTo(self) - } - - // Background button - bgButton.layer.cornerRadius = layer.cornerRadius - bgButton.clipsToBounds = true - bgButton.addTarget(self, action: #selector(bgButtonTapAction), for: .touchUpInside) - let bgButtonImageColor: UIColor = .c.cttn - let image = MWIconFont.image(fromIcon: .iconUploadimg, size: CGSize(width: 48, height: 48), color: bgButtonImageColor) - //MWIconFont.image(fromIconInt: .iconUploadimg, size: CGSize(width: 48, height: 48), color: bgButtonImageColor, edgeInsets: .zero) - bgButton.setImage(image, for: .normal) - bgButton.setTitle("Click to generate images", for: .normal) - bgButton.setTitleColor(.c.cttn, for: .normal) - bgButton.titleLabel?.font = .t.tbm - //EPSystemToken.typography(.txtBodyM).font - addSubview(bgButton) - bgButton.snp.makeConstraints { make in - make.edges.equalTo(self) - } - - // Image view - imageView.layer.cornerRadius = 8 - imageView.clipsToBounds = true - imageView.layer.borderColor = UIColor.c.civn.cgColor - imageView.isHidden = true - imageView.contentMode = .scaleAspectFill - addSubview(imageView) - imageView.snp.makeConstraints { make in - make.edges.equalTo(self) - } - - // Loading view - loadingView.isHidden = true - loadingView.backgroundColor = UIColor.black.withAlphaComponent(0.7) // Replaced hex_120E1B_bg - addSubview(loadingView) - loadingView.snp.makeConstraints { make in - make.edges.equalTo(self) - } - - // Indicator view - indicatorView.color = .white // Replaced hex_FFFFFF - loadingView.addSubview(indicatorView) - indicatorView.snp.makeConstraints { make in - make.centerX.equalTo(loadingView) - make.centerY.equalTo(loadingView).offset(-10) - } - - // Indicator label - indicatorLabel.font = .t.tbm//EPSystemToken.typography(.txtBodyM).font - indicatorLabel.textColor = .c.ctpsn//EPSystemToken.color(.txtPrimarySpecialmapNormal) - indicatorLabel.text = NSLocalizedString("uploading", comment: "") - loadingView.addSubview(indicatorLabel) - indicatorLabel.snp.makeConstraints { make in - make.centerX.equalTo(loadingView) - make.top.equalTo(indicatorView.snp.bottom).offset(12) - } - - // Error view - errorView.isHidden = true - errorView.backgroundColor = .red //EPSystemToken.color(.importantOnpicNormal) - addSubview(errorView) - errorView.snp.makeConstraints { make in - make.leading.trailing.bottom.equalTo(self) - } - - // Error label - errorLabel.isHidden = false - errorLabel.font = .t.tbs//EPSystemToken.typography(.txtBodyS).font - errorLabel.textColor = .c.ctpsn//EPSystemToken.color(.txtPrimarySpecialmapNormal) - errorLabel.numberOfLines = 5 - errorLabel.textAlignment = .left - errorView.addSubview(errorLabel) - errorLabel.snp.makeConstraints { make in - make.leading.equalTo(errorView).offset(16) - make.trailing.equalTo(errorView).offset(-16) - make.top.equalTo(errorView).offset(8) - make.bottom.equalTo(errorView).offset(-8) - } - - // Delete button - let deleteButtonSize = deleteButton.bgImageSize() - deleteButton.addTarget(self, action: #selector(deleteAction), for: .touchUpInside) - addSubview(deleteButton) - deleteButton.snp.makeConstraints { make in - make.trailing.equalTo(self).offset(-16) - make.top.equalTo(self).offset(16) - make.size.equalTo(deleteButtonSize) - } - - // Retry button - retryButton.isHidden = true - - let retryImage = MWIconFont.image(fromIcon: .iconReload, size: CGSize(width: 48, height: 48), color: UIColor.c.ctpsn) - retryButton.setImage(retryImage, for: .normal) - retryButton.addTarget(self, action: #selector(retryAction), for: .touchUpInside) - addSubview(retryButton) - retryButton.snp.makeConstraints { make in - make.center.equalTo(self) - make.size.equalTo(CGSize(width: 48, height: 48)) - } - - state = .default - } - - // MARK: - Public Methods - func setupVideoMode() { - videoMode = true - } - - // MARK: - Actions - @objc private func bgButtonTapAction() { - bgTapBlock?() - } - - @objc private func deleteAction() { - deleteTapBlock?() - } - - @objc private func retryAction() { - retryTapBlock?() - } - - // MARK: - State Handling - private func dealUploadState(_ state: UploadImageState) { - indicatorView.stopAnimating() - imageView.isHidden = false - imageView.layer.borderWidth = 0 - errorLabel.text = "" - bgButton.isHidden = true - - switch state { - case .default: - bgButton.isHidden = false - bgButton.isEnabled = true - imageView.isHidden = true - deleteButton.isHidden = true - retryButton.isHidden = true - loadingView.isHidden = true - errorView.isHidden = true - case .uploading: - bottomBottom.isEnabled = false - deleteButton.isHidden = false - retryButton.isHidden = true - loadingView.isHidden = false - errorView.isHidden = true - indicatorView.startAnimating() - case .failed: - bottomBottom.isEnabled = false - deleteButton.isHidden = false - retryButton.isHidden = false - loadingView.isHidden = true - errorView.isHidden = false - imageView.layer.borderWidth = CLSystemToken.border(token: .bs)//EPSystemToken.border(.borderS) - case .oversize: - bottomBottom.isEnabled = false - deleteButton.isHidden = false - retryButton.isHidden = true - loadingView.isHidden = true - errorView.isHidden = false - imageView.layer.borderWidth = CLSystemToken.border(token: .bs)//EPSystemToken.border(.borderS) - case .yellow: - bottomBottom.isEnabled = false - deleteButton.isHidden = false - retryButton.isHidden = true - loadingView.isHidden = true - errorView.isHidden = false - imageView.layer.borderWidth = CLSystemToken.border(token: .bs) - case .success: - bottomBottom.isEnabled = true - deleteButton.isHidden = false - retryButton.isHidden = true - loadingView.isHidden = true - errorView.isHidden = true - } - } - - // MARK: - Overrides - override func layoutSubviews() { - super.layoutSubviews() - bgButton.setUp(.top, padding: 12) - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/ViewsNumbers.swift b/crush/Crush/Src/Components/UI/BaseView/ViewsNumbers.swift deleted file mode 100644 index a4d2ddb..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/ViewsNumbers.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Float.swift -// Crush -// -// Created by Leon on 2025/7/17. -// -import SnapKit - -class Layout{ - public var lr: CGFloat { - return 24 - } -} - -extension ConstraintOffsetTarget { - var lr: CGFloat { - return 24 - // CGFloat(self) - } - - static var lrs : CGFloat{ - return 24 - } -} diff --git a/crush/Crush/Src/Components/UI/BaseView/VoiceSelectView.swift b/crush/Crush/Src/Components/UI/BaseView/VoiceSelectView.swift deleted file mode 100644 index 965c8c5..0000000 --- a/crush/Crush/Src/Components/UI/BaseView/VoiceSelectView.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// VoiceSelectView.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -/// 大块block,竖着排列:语言图标 + 文字 -class VoiceSelectView:UIView{ - - var iv: CLImageView! - var titleLabel: UILabel! - var topButton: UIButton! - - var topTapBlock: (() -> Void)? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews(){ - backgroundColor = .c.csen - layer.cornerRadius = 16 - clipsToBounds = true - - iv = { - let v = CLImageView(frame: .zero) - v.image = MWIconFont.image(fromIcon: .voice, size: CGSize(width: 24, height: 24), color: .c.ctsn) - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalToSuperview().offset(24) - make.size.equalTo(CGSize(width: 24, height: 24)) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.tbm - v.textColor = .c.ctsn - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(iv.snp.bottom).offset(16) - make.bottom.equalToSuperview().offset(-24) - } - return v - }() - - titleLabel.text = "Select 1 voice from below" - - topButton = { - let v = UIButton() - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - v.addTarget(self, action: #selector(topButtonTap), for: .touchUpInside) - return v - }() - } - - @objc private func topButtonTap(){ - topTapBlock?() - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/AudioButton.swift b/crush/Crush/Src/Components/UI/Buttons/AudioButton.swift deleted file mode 100644 index e6767fc..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/AudioButton.swift +++ /dev/null @@ -1,445 +0,0 @@ -// -// AudioButton.swift -// Crush -// -// Created by Leon on 2025/8/1. -// - -import UIKit -import SnapKit -import Lottie - -enum AudioButtonEnumSize: Int { - /// height: 48 - case large - /// height: 32 - case medium - /// height: 32 - case small - /// 20x20 - case square20 - case square24 -} - -// Define AudioButtonStyle enum -enum AudioButtonStyle: Int { - // Clear bg. used in AI Role create process, select voice - case clearBg - /// White triangle + semi-transparent black circle background - case videoPlay - /// 白色三角 + 主体背景色 - case voiceThemePlay -} - -class AudioButton: CLButton { - - - // MARK: - Properties - var bgImageView: UIImageView! - var leftImageView: UIImageView! - var voiceAnimation: LottieAnimationView! - - private var indicator: IndicatorView! - private var buttonSize: AudioButtonEnumSize = .medium - private var path: String = "" - private var iconSize: CGSize = CGSize(width: 16, height: 16) - // MARK: - States - var isDisplay: Bool = true { - didSet { - isHidden = !isDisplay - } - } - - var indicatorAnimated: Bool { - return indicator.isAnimating - } - - var audioLottieAnimated: Bool{ - return voiceAnimation.isAnimationPlaying - } - - // MARK: - Initialization - static func buttonWithSize(_ size: AudioButtonEnumSize) -> AudioButton { - return AudioButton(size: size) - } - - static func buttonWithStyle(_ style: AudioButtonStyle) -> AudioButton { - return AudioButton(style: style) - } - - convenience init(size: AudioButtonEnumSize) { - self.init(frame: .zero) - self.buttonSize = size - initialViews() - } - - convenience init(style: AudioButtonStyle, size:AudioButtonEnumSize = .square20) { - self.init(frame: .zero) - switch style { - case .videoPlay: - initialCirclePlayStyle() - case .clearBg: - initialClearBgPlayStyle() - case .voiceThemePlay: - initialVoiceThemePlay() - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - // Default initialization, but we discourage direct init - // assertionFailure("Use buttonWithSize or buttonWithStyle instead!") - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - self.buttonSize = .medium - initialViews() - } - - // MARK: - 🚩Style 1: Traditional Audio Button - private func initialViews() { - isDisplay = true - var width: CGFloat = 88 - var height: CGFloat = 48 - var cornerRadius: CGFloat = 24 - var imageEdgeInsets: UIEdgeInsets = .zero - - switch buttonSize { - case .large: - height = 48 - cornerRadius = 24 - imageEdgeInsets = UIEdgeInsets(top: 0, left: 32, bottom: 0, right: 32) - iconSize = CGSize(width: 20, height: 20) - case .small: - height = 32 - width = 52 - cornerRadius = 16 - imageEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) - iconSize = CGSize(width: 16, height: 16) - case .medium: - height = 32 - cornerRadius = 16 - imageEdgeInsets = UIEdgeInsets(top: 0, left: 32, bottom: 0, right: 32) - iconSize = CGSize(width: 16, height: 16) - case .square20: - height = 20 - cornerRadius = 0 - imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - iconSize = CGSize(width: height, height: height) - case .square24: - height = 24 - cornerRadius = 0 - imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - iconSize = CGSize(width: height, height: height) - } - - snp.makeConstraints { make in - make.height.equalTo(height) - } - layer.cornerRadius = cornerRadius - clipsToBounds = true - self.imageEdgeInsets = imageEdgeInsets - - bgImageView = UIImageView() - bgImageView.clipsToBounds = true - bgImageView.image = UIImage.withColor(color: .purple, size: CGSize(width: width, height: 24)) - addSubview(bgImageView) - bgImageView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - leftImageView = UIImageView() - leftImageView.image = iconOfWhitePlay() - addSubview(leftImageView) - leftImageView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 14, height: 14)) - } - - let animation = LottieAnimation.named("voice_white") - voiceAnimation = LottieAnimationView(animation: animation) - // voiceAnimation = LottieAnimationView.animationNamed("voice_white.json") - voiceAnimation.isUserInteractionEnabled = false - voiceAnimation.loopMode = .loop - voiceAnimation.animationSpeed = 1.4 - voiceAnimation.contentMode = .scaleAspectFit - voiceAnimation.isHidden = true - voiceAnimation.tintColor = .white - addSubview(voiceAnimation) - voiceAnimation.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 14, height: 14)) - } - - indicator = IndicatorView(color: .white) - indicator.size = CGSize(width: 12, height: 12) - indicator.isHidden = true - addSubview(indicator) - indicator.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 12, height: 12)) - } - } - - // MARK: - Style(Custom) 2: Circle Play Style - private func initialCirclePlayStyle() { - let width: CGFloat = 32 - iconSize = CGSize(width: 16, height: 16) - - snp.makeConstraints { make in - make.height.equalTo(32) - } - layer.cornerRadius = 16 - clipsToBounds = true - - bgImageView = UIImageView() - bgImageView.clipsToBounds = true - bgImageView.image = UIImage.withColor(color: UIColor.black.withAlphaComponent(0.5), size: CGSize(width: width, height: width)) - addSubview(bgImageView) - bgImageView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - leftImageView = UIImageView() - leftImageView.image = iconOfWhitePlay() - addSubview(leftImageView) - leftImageView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 16, height: 16)) - } - - let animation = LottieAnimation.named("voice_white") - voiceAnimation = LottieAnimationView(animation: animation) - //voiceAnimation = EPLottieAnimationView.animationNamed("voice_white.json") - voiceAnimation.isUserInteractionEnabled = false - voiceAnimation.loopMode = .loop - voiceAnimation.animationSpeed = 1.4 - voiceAnimation.contentMode = .scaleAspectFit - voiceAnimation.isHidden = true - voiceAnimation.tintColor = .white - addSubview(voiceAnimation) - voiceAnimation.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 16, height: 16)) - } - - indicator = IndicatorView(color: .white) - indicator.size = CGSize(width: 16, height: 16) - indicator.isHidden = true - addSubview(indicator) - indicator.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 16, height: 16)) - } - } - - // MARK: - Style(Custom) 3: Clear bg - private func initialClearBgPlayStyle() { - iconSize = CGSize(width: 20, height: 20) - touchAreaInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) - snp.makeConstraints { make in - make.width.equalTo(20) - make.height.equalTo(20) - } - layer.cornerRadius = 0 - clipsToBounds = true - - leftImageView = UIImageView() - leftImageView.image = iconOfWhitePlay() - addSubview(leftImageView) - leftImageView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 16, height: 16)) - } - - let animation = LottieAnimation.named("voice_white")// - voiceAnimation = LottieAnimationView(animation: animation) - voiceAnimation.isUserInteractionEnabled = false - voiceAnimation.loopMode = .loop - voiceAnimation.animationSpeed = 1.4 - voiceAnimation.contentMode = .scaleAspectFit - voiceAnimation.isHidden = true - voiceAnimation.tintColor = .black - addSubview(voiceAnimation) - voiceAnimation.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 16, height: 16)) - } - - indicator = IndicatorView(color: .white) // .withAlphaComponent(0.5) - indicator.size = CGSize(width: 16, height: 16) - indicator.isHidden = true - addSubview(indicator) - indicator.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 16, height: 16)) - } - } - - private func initialVoiceThemePlay(){ - iconSize = CGSize(width: 12, height: 12) - touchAreaInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) - snp.makeConstraints { make in - make.width.equalTo(24) - make.height.equalTo(24) - } - layer.cornerRadius = 12 - backgroundColor = .c.cpvn - clipsToBounds = true - - leftImageView = UIImageView() - leftImageView.image = iconOfWhitePlay() - addSubview(leftImageView) - leftImageView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 12, height: 12)) - } - - let animation = LottieAnimation.named("voice_white")// - voiceAnimation = LottieAnimationView(animation: animation) - voiceAnimation.isUserInteractionEnabled = false - voiceAnimation.loopMode = .loop - voiceAnimation.animationSpeed = 1.4 - voiceAnimation.contentMode = .scaleAspectFit - voiceAnimation.isHidden = true - voiceAnimation.tintColor = .black - addSubview(voiceAnimation) - voiceAnimation.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 12, height: 12)) - } - - indicator = IndicatorView(color: .white) // .withAlphaComponent(0.5) - indicator.size = CGSize(width: 12, height: 12) - indicator.isHidden = true - addSubview(indicator) - indicator.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 12, height: 12)) - } - } - - // MARK: - Icons - func iconOfWhitePlay() -> UIImage? { - return MWIconFont.image(fromIcon: .play, size: self.iconSize, color: .white) - } - - func iconOfWhiteVoice() -> UIImage? { - return MWIconFont.image(fromIcon: .voiceLive, size: self.iconSize, color: .white) - } - - func blackIcon() -> UIImage? { - return MWIconFont.image(fromIcon: .play, size: self.iconSize, color: .c.csbn) - } - - // MARK: - Public Methods - func startIndicator() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - if self.indicator.isAnimating { - self.leftImageView.isHidden = true - self.voiceAnimation.isHidden = true - return - } - self.indicator.startAnimating() - self.leftImageView.isHidden = true - self.voiceAnimation.isHidden = true - } - } - - func stopIndicator() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - if !self.indicator.isAnimating { - self.leftImageView.isHidden = false - self.voiceAnimation.isHidden = true - return - } - self.leftImageView.isHidden = false - self.voiceAnimation.isHidden = true - self.indicator.stopAnimating() - } - } - - func startAnimation() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - if self.voiceAnimation.isAnimationPlaying { - return - } - self.voiceAnimation.isHidden = false - self.leftImageView.isHidden = true - self.voiceAnimation.play() - } - } - - func stopAnimation() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - if !self.voiceAnimation.isAnimationPlaying { - return - } - self.voiceAnimation.stop() - self.voiceAnimation.isHidden = true - self.leftImageView.isHidden = false - } - } - - func reloadState(with model: SpeechModel?) { - guard let speechModel = model else{ - reloadViews(model: model) - return - } - self.path = speechModel.path - speechModel.stateChangedBlock = { [weak self] updatedModel in - guard let self = self, self.path == updatedModel.path else { return } - self.reloadViews(model: updatedModel) - self.checkState(model: updatedModel) - } - reloadViews(model: speechModel) - } - - // MARK: - Private Methods - private func reloadViews(model: SpeechModel?) { - guard let audioModel = model else{ - stopIndicator() - stopAnimation() - return - } - if audioModel.loadState == .loading { - startIndicator() - } else { - stopIndicator() - } - if audioModel.playState == .playing { - startAnimation() - } else { - stopAnimation() - } - } - - private func checkState(model: SpeechModel) { - guard model.loadState == .complete else { - // SpeechManager.shared.stopPlay(with: model) // ❌ - return - } - - switch model.playState { - case .default: - if model.canAutoPlay && isDisplay { - SpeechManager.shared.startPlay(with: model) - } - case .complete, .failed: - SpeechManager.shared.stopPlay(with: model) - case .playing: - break - } - } - - // MARK: - Deinit - deinit { - // 🔥 Replacing DLog with print - print("♻️ AudioButton dealloc") - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/CLButton.swift b/crush/Crush/Src/Components/UI/Buttons/CLButton.swift deleted file mode 100644 index cdc33f0..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/CLButton.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// CLButton.swift -// Crush -// -// Created by Leon on 2025/9/25. -// - -import UIKit - -class CLButton: UIButton { - - /// 点击间隔(秒),默认 0.8s - var clickInterval: TimeInterval = 0.8 - - private var isIgnoringEvent = false - - override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { - if isIgnoringEvent { - // 正在防抖,丢弃点击 - return - } - - if clickInterval > 0 { - isIgnoringEvent = true - DispatchQueue.main.asyncAfter(deadline: .now() + clickInterval) { [weak self] in - self?.isIgnoringEvent = false - } - } - - super.sendAction(action, to: target, for: event) - } -} - - -class CLControl: UIControl { - - /// 点击间隔(秒),默认 0.8s - @IBInspectable var clickInterval: TimeInterval = 0.8 - - private var isIgnoringEvent = false - - override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { - if isIgnoringEvent { - // 正在防抖,丢弃点击 - return - } - - if clickInterval > 0 { - isIgnoringEvent = true - DispatchQueue.main.asyncAfter(deadline: .now() + clickInterval) { [weak self] in - self?.isIgnoringEvent = false - } - } - - super.sendAction(action, to: target, for: event) - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/ChipButtons.swift b/crush/Crush/Src/Components/UI/Buttons/ChipButtons.swift deleted file mode 100644 index 621f436..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/ChipButtons.swift +++ /dev/null @@ -1,566 +0,0 @@ -// -// ChipButtons.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -import SnapKit -import UIKit - -// MARK: - Base Class - -class EPChipButton: CLControl { - // MARK: - Constants - - let chipButtonHeight: CGFloat = 32 - - // MARK: - Properties - - lazy var stack: UIStackView = { - let stack = UIStackView() - stack.alignment = .fill - stack.distribution = .fill - stack.alignment = .center - stack.axis = .horizontal - stack.spacing = 8 - return stack - }() - - lazy var imageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - imageView.isUserInteractionEnabled = true - return imageView - }() - - lazy var textLabel: UILabel = { - let label = UILabel() - label.font = CLSystemToken.font(token: .tlm) - label.isUserInteractionEnabled = true - return label - }() - - lazy var iconView: UIImageView = { - let iconView = UIImageView() - iconView.contentMode = .scaleAspectFit - iconView.isUserInteractionEnabled = true - return iconView - }() - - var text: String? { - didSet { - textLabel.text = text - setNeedsLayout() - } - } - - var iconCode: IconCode = .sort { - didSet { - iconView.isHidden = (iconCode.rawValue <= 0) - iconView.image = defaultIcon - } - } - - var defaultIconColor: UIColor? { - didSet { - iconView.image = defaultIcon - } - } - - var defaultIcon: UIImage? { - let color = defaultIconColor ?? UIColor.c.ctsn - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - - var selectedIcon: UIImage? { - nil // Subclasses override - } - - var defaultImage: UIImage? { - nil // Subclasses override - } - - var selectedImage: UIImage? { - nil // Subclasses override - } - - var paddingInsets: UIEdgeInsets { - .zero // Subclasses override - } - - var iconSize: CGSize { - .zero // Subclasses override - } - - var imageSize: CGSize { - .zero // Subclasses override - } - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - clipsToBounds = true - layer.cornerRadius = chipButtonHeight * 0.5 - - setupTheme() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - func setupTheme() { - textLabel.textColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - } - - // MARK: - Hit Testing - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - guard isUserInteractionEnabled, alpha != 0, !isHidden else { return nil } - - if self.point(inside: point, with: event) { - return self - } - return super.hitTest(point, with: event) - } -} - -// MARK: - EPChipFilterButton - -/// 只有文字,左右16。 height 32 -class EPChipFilterButton: EPChipButton { - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .c.csen // EPSystemToken.color(.surfaceElementNormal) - layer.borderWidth = 0 - - addSubview(textLabel) - textLabel.snp.makeConstraints { make in - make.edges.equalTo(self).inset(paddingInsets).priority(.high) - make.height.equalTo(chipButtonHeight) - } - } - - override func setupTheme() { - super.setupTheme() - layer.borderColor = UIColor.c.cpvn.cgColor // EPSystemToken.color(.primaryVariantNormal).cgColor - } - - override var paddingInsets: UIEdgeInsets { - UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) - } - - override var intrinsicContentSize: CGSize{ - return CGSize(width: textLabel.size.width + 32, height: chipButtonHeight) - } - - override var isHighlighted: Bool { - didSet { - backgroundColor = isSelected - ? (isHighlighted ? UIColor.c.cpp : UIColor.c.cpn) - : (isHighlighted ? UIColor.c.csep : UIColor.c.csen) - } - } - - override var isSelected: Bool { - didSet { - backgroundColor = isSelected - ? UIColor.c.cpn - : UIColor.c.csen - layer.borderWidth = isSelected ? 1 : 0 - } - } - - override var isEnabled: Bool { - didSet { - textLabel.textColor = isEnabled - ? UIColor.c.ctpn - : UIColor.c.ctd - backgroundColor = isEnabled - ? UIColor.c.csen - : UIColor.c.csed - } - } -} - -class EPChipCenterIconButton: EPChipButton { - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .c.csen // EPSystemToken.color(.surfaceElementNormal) - layer.borderWidth = 0 - defaultIconColor = .c.ctpn - - addSubview(iconView) - iconView.snp.makeConstraints { make in - make.center.equalToSuperview() - // make.height.equalTo(chipButtonHeight) - } - } - - override func setupTheme() { - super.setupTheme() - layer.borderColor = UIColor.c.cpvn.cgColor // EPSystemToken.color(.primaryVariantNormal).cgColor - } - - override var iconSize: CGSize { - return .init(width: 20, height: 20) - } - - override var isHighlighted: Bool { - didSet { - backgroundColor = isSelected - ? (isHighlighted ? UIColor.c.cpp : UIColor.c.cpn) - : (isHighlighted ? UIColor.c.csep : UIColor.c.csen) - } - } - - override var isSelected: Bool { - didSet { - backgroundColor = isSelected - ? UIColor.c.cpn - : UIColor.c.csen - layer.borderWidth = isSelected ? 1 : 0 - } - } - - override var isEnabled: Bool { - didSet { - textLabel.textColor = isEnabled - ? UIColor.c.ctpn - : UIColor.c.ctd - backgroundColor = isEnabled - ? UIColor.c.csen - : UIColor.c.csed - } - } - - override func layoutSubviews() { - super.layoutSubviews() - layer.cornerRadius = bounds.size.height * 0.5 - } -} - -// MARK: - EPChipDropdownButton - -class EPChipDropdownButton: EPChipButton { - var needTransform: Bool = true - - override init(frame: CGRect) { - super.init(frame: frame) - stack.spacing = 4 - - addSubview(stack) - stack.snp.makeConstraints { make in - make.edges.equalTo(self).inset(paddingInsets) - make.height.equalTo(chipButtonHeight) - } - - stack.addArrangedSubview(textLabel) - stack.addArrangedSubview(iconView) - iconView.snp.makeConstraints { make in - make.size.equalTo(iconSize) - } - } - - override func setupTheme() { - super.setupTheme() - textLabel.textColor = isSelected - ? UIColor.c.cpvn - : UIColor.c.ctpn - iconView.image = isSelected ? selectedIcon : defaultIcon - } - - override var paddingInsets: UIEdgeInsets { - UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 8) - } - - override var iconSize: CGSize { - CGSize(width: 12, height: 12) - } - - override var defaultIcon: UIImage? { - let color = UIColor.c.ctsn // EPSystemToken.color(.txtSecondaryNormal) - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - - override var selectedIcon: UIImage? { - let color = UIColor.c.cpvn // EPSystemToken.color(.primaryVariantNormal) - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - - override var isHighlighted: Bool { - didSet { - backgroundColor = isHighlighted ? UIColor.c.csep : nil - } - } - - override var isSelected: Bool { - didSet { - textLabel.textColor = isSelected - ? UIColor.c.cpvn - : UIColor.c.ctpn - - UIView.animate(withDuration: 0.2) { - if self.needTransform { - self.iconView.transform = self.isSelected ? CGAffineTransform(rotationAngle: .pi) : .identity - } - self.iconView.image = self.isSelected ? self.selectedIcon : self.defaultIcon - } - } - } -} - -// MARK: - EPChipRemovableButton - -class EPChipRemovableButton: EPChipButton { - override init(frame: CGRect) { - super.init(frame: frame) - stack.spacing = 8 - - addSubview(stack) - stack.snp.makeConstraints { make in - make.edges.equalTo(self).inset(paddingInsets).priority(998) - make.height.equalTo(chipButtonHeight) - } - - stack.addArrangedSubview(textLabel) - stack.addArrangedSubview(iconView) - iconView.snp.makeConstraints { make in - make.size.equalTo(iconSize) - } - } - - override func setupTheme() { - super.setupTheme() - backgroundColor = .c.csen - iconView.image = defaultIcon - } - - override var paddingInsets: UIEdgeInsets { - UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) - } - - override var iconSize: CGSize { - CGSize(width: 16, height: 16) - } - - override var defaultIcon: UIImage? { - let color = UIColor.c.ctsn // EPSystemToken.color(.txtSecondaryNormal) - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - - override var selectedIcon: UIImage? { - let color = UIColor.c.cpvn - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - - override var isHighlighted: Bool { - didSet { - backgroundColor = isHighlighted - ? UIColor.c.csep - : UIColor.c.csen - } - } - - func widthPreCal(tagHeight: CGFloat) -> CGFloat { - let paddingInsets = self.paddingInsets - return textLabel.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: tagHeight)).width + - paddingInsets.left + paddingInsets.right + iconSize.width + stack.spacing - } - - func setupHasImageMode() { - stack.insertArrangedSubview(imageView, at: 0) - imageView.isHidden = false - imageView.snp.makeConstraints { make in - make.size.equalTo(imageSize) - } - } -} - -// MARK: - EPChipAssistButton - -/// 图标在左,文字在右(用处eg:Interests添加页面Add按钮) -class EPChipAssistButton: EPChipButton { - var iconTextSpacing: CGFloat = 8 { - didSet { - stack.spacing = iconTextSpacing - } - } - - var buttonHeight: CGFloat = 32 { - didSet { - layer.cornerRadius = buttonHeight * 0.5 - stack.snp.updateConstraints { make in - make.height.equalTo(buttonHeight) - } - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - stack.spacing = 8 - - addSubview(stack) - stack.snp.makeConstraints { make in - make.edges.equalTo(self).inset(paddingInsets) - make.height.equalTo(chipButtonHeight) - } - - stack.addArrangedSubview(iconView) - stack.addArrangedSubview(textLabel) - iconView.snp.makeConstraints { make in - make.size.equalTo(iconSize) - } - } - - override func setupTheme() { - super.setupTheme() - backgroundColor = .c.csen // EPSystemToken.color(.surfaceElementNormal) - iconView.image = defaultIcon - } - - override var paddingInsets: UIEdgeInsets { - UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) - } - - override var iconSize: CGSize { - CGSize(width: 16, height: 16) - } - - override var defaultIcon: UIImage? { - let color = defaultIconColor ?? UIColor.c.ctsn // EPSystemToken.color(.txtSecondaryNormal) - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - -// override var isHighlighted: Bool { -// didSet { -// backgroundColor = isHighlighted -// ? UIColor.c.csep -// : UIColor.c.csen -// } -// } - - override var isSelected: Bool { - didSet { - backgroundColor = isSelected - ? UIColor.c.cpn - : UIColor.c.csen - //layer.borderWidth = isSelected ? 1 : 0 - } - } - - func setDefaultImage(_ image: UIImage?) { - iconView.image = image - } - - func setIconSize(_ size: CGSize) { - iconView.snp.updateConstraints { make in - make.size.equalTo(size) - } - } - - func widthPreCal(tagHeight: CGFloat) -> CGFloat { - let paddingInsets = self.paddingInsets - return textLabel.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: tagHeight)).width + - paddingInsets.left + paddingInsets.right + iconSize.width + stack.spacing - } -} - -// MARK: - EPChipContrastButton:EPChipAssistButton -/// Like EPIconContrastTertiaryButton的加文字版本, 特别处理 -class EPChipContrastButton:EPChipAssistButton { - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .c.csedn - buttonHeight = 48 - defaultIconColor = .white - textLabel.font = .t.tll - } - - override var paddingInsets: UIEdgeInsets { - UIEdgeInsets(top: 0, left: 32, bottom: 0, right: 32) - } - - override var iconSize: CGSize { - CGSize(width: 24, height: 24) - } -} - - -// MARK: - EPChipImageIconButton - -/// 左边image+中间文字+右边iconFont -class EPChipImageIconButton: EPChipFilterButton { - override init(frame: CGRect) { - super.init(frame: frame) - stack.spacing = 8 - - addSubview(stack) - stack.snp.makeConstraints { make in - make.edges.equalTo(self).inset(paddingInsets) - make.height.equalTo(chipButtonHeight) - } - - stack.addArrangedSubview(imageView) - stack.addArrangedSubview(textLabel) - stack.addArrangedSubview(iconView) - imageView.snp.makeConstraints { make in - make.size.equalTo(imageSize) - } - iconView.snp.makeConstraints { make in - make.size.equalTo(iconSize) - } - - imageView.isHidden = true - iconView.isHidden = true - } - - override var paddingInsets: UIEdgeInsets { - UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) - } - - override var iconSize: CGSize { - CGSize(width: 16, height: 16) - } - - override var imageSize: CGSize { - CGSize(width: 16, height: 16) - } - - override var defaultIcon: UIImage? { - let color = UIColor.c.ctsn // EPSystemToken.color(.txtSecondaryNormal) - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - - override var selectedIcon: UIImage? { - let color = UIColor.c.cswn // EPSystemToken.color(.surfaceWhiteNormal) - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: iconSize, color: color, edgeInsets: .zero) - } - - override var isSelected: Bool { - didSet { - iconView.image = isSelected ? selectedIcon : defaultIcon - imageView.image = isSelected ? selectedImage : defaultImage - } - } - - func reloadPadding(showImage: Bool, showIcon: Bool) { - var leftPadding: CGFloat = 16 - var rightPadding: CGFloat = 16 - if showImage { - leftPadding = 12 - } - if showIcon { - rightPadding = 12 - } - stack.snp.remakeConstraints { make in - make.edges.equalTo(self).inset(UIEdgeInsets(top: 0, left: leftPadding, bottom: 0, right: rightPadding)) - make.height.equalTo(chipButtonHeight) - } - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/CommonUploadImageButton.swift b/crush/Crush/Src/Components/UI/Buttons/CommonUploadImageButton.swift deleted file mode 100644 index 038c830..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/CommonUploadImageButton.swift +++ /dev/null @@ -1,285 +0,0 @@ -// -// CommonUploadImageButton.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import UIKit -import SnapKit - -enum CommentUploadState: Int { - case loading - case success - case failed - case checkdFailed -} - -class CommonUploadImageButton: UIButton { - private var indicatorBGView: UIView! - private var indicatorView: UIActivityIndicatorView! - private var retryButton: UIButton! - private var errorButton: UIButton! - private var contentImageView: UIImageView! - private var closeButton: EPIconTertiaryDarkButton! - - @Published private(set) var uploadState: CommentUploadState = .loading { - didSet { - reloadViews() - } - } - private(set) var imageUrlNowIfHave: String? - var uploadModel: CommentImageModel? - - var optionErrorMsg: String? - var closeAction: ((CommonUploadImageButton) -> Void)? - var tapRetryAction: ((CommonUploadImageButton) -> Void)? - - var uploadSuceess: ((CommonUploadImageButton) -> Void)? - var uploadOrCheckFailed: ((CommonUploadImageButton) -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - initializeViews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - initializeViews() - } - - private func initializeViews() { - layer.cornerRadius = 8.0 - layer.masksToBounds = true - - contentImageView = { - let imageView = UIImageView() - addSubview(imageView) - imageView.contentMode = .scaleAspectFill - imageView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return imageView - }() - - indicatorBGView = { - let view = UIView() - addSubview(view) - view.isHidden = true - view.layer.borderColor = UIColor.c.civn.cgColor//EPSystemToken.color(.important_variant_normal).cgColor - view.layer.cornerRadius = layer.cornerRadius - view.backgroundColor = UIColor.c.csedn//EPSystemToken.color(.surface_element_dark_normal) - view.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return view - }() - - indicatorView = { - let indicator = UIActivityIndicatorView() - addSubview(indicator) - indicator.color = .c.ctpsn//EPSystemToken.color(.txt_primary_specialmap_normal) - indicator.isHidden = true - indicator.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 24, height: 24)) - } - return indicator - }() - - closeButton = { - let button = EPIconTertiaryDarkButton(radius: .round, iconSize: .xxs, iconCode: .delete) - addSubview(button) - button.isHidden = true - button.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - button.addTarget(self, action: #selector(closeButtonAction), for: .touchUpInside) - button.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-8) - make.top.equalToSuperview().offset(8) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - return button - }() - - retryButton = { - let button = UIButton(type: .custom) - addSubview(button) - button.isHidden = true - let image = MWIconFont.image(fromIcon: .iconReload, size: .init(width: 16, height: 16), color: .c.ctpsn) - button.setImage(image, for: .normal) - button.contentMode = .center - button.touchAreaInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) - button.addTarget(self, action: #selector(retryButtonAction), for: .touchUpInside) - button.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 32, height: 32)) - } - return button - }() - - errorButton = { - let button = UIButton(type: .custom) - addSubview(button) - button.isHidden = true - button.setImage(UIImage(named: "album_reupload"), for: .normal) - button.touchAreaInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) - button.addTarget(self, action: #selector(errorButtonAction), for: .touchUpInside) - button.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 32, height: 32)) - } - return button - }() - - setupImageDefaultState() - } - - private func reloadViews() { - indicatorBGView.isHidden = true - indicatorBGView.layer.borderWidth = 0 - if indicatorView.isAnimating { - indicatorView.stopAnimating() - } - retryButton.isHidden = true - closeButton.isHidden = true - errorButton.isHidden = true - - switch uploadState { - case .loading: - indicatorBGView.isHidden = false - indicatorView.startAnimating() - case .failed: - indicatorBGView.isHidden = false - retryButton.isHidden = false - closeButton.isHidden = false - indicatorBGView.layer.borderWidth = CLSystemToken.border(token: .bs)//EPSystemToken.border(.s) - case .success: - closeButton.isHidden = false - case .checkdFailed: - indicatorBGView.isHidden = false - closeButton.isHidden = false - errorButton.isHidden = false - indicatorBGView.layer.borderWidth = CLSystemToken.border(token: .bs)//EPSystemToken.border(.s) - } - - if imageView?.image != nil { - setImage(nil, for: .normal) - } - } - - // MARK: - Public Methods - - func setupImageDefaultState() { - - let image = MWIconFont.image(fromIcon: .iconUploadimg, size: .init(width: 24, height: 24), color: .c.cttn) - setImage(image, for: .normal) - backgroundColor = .c.csen//EPSystemToken.color(.surface_element_normal) - - contentImageView.image = nil - imageUrlNowIfHave = nil - closeButton.isHidden = true - indicatorBGView.isHidden = true - errorButton.isHidden = true - } - - @discardableResult - func bindViewImage(_ image: UIImage, report: Bool = false, checkImage: Bool = false) -> UploadPhotoM { - contentImageView.image = image - uploadState = .loading - DispatchQueue.main.async {[weak self] in - // UploadPhotoM is returned finally - self?.uploadState = .loading - } - - let model = CommentImageModel() - model.isReport = report - uploadModel = model - let photo = model.startUpload(with: image, checkImage: checkImage) - uploadModel?.imageUploadComplete = { [weak self] result, photo in - guard let self = self else { return } - if result { - self.uploadState = .success - self.uploadSuceess?(self) - } else if photo.imageChecked && photo.imageCheckIsViolation { - self.uploadState = .checkdFailed - self.uploadOrCheckFailed?(self) - } else { - self.uploadState = .failed - self.uploadOrCheckFailed?(self) - } - self.reloadViews() - } - reloadViews() - return photo - } - - func bindViewImageNoUpload(_ image: UIImage) { - contentImageView.image = image - uploadState = .loading - reloadViews() - } - - func bindViewImage(_ image: UIImage?) { - guard let img = image else{ - return - } - contentImageView.image = img - } - - func bindSuccessImage(_ image: UIImage?) { - guard let img = image else{ - return - } - contentImageView.image = img - uploadState = .success - } - - func bindViewImageUrl(_ imageUrl: String) { - imageUrlNowIfHave = imageUrl - //contentImageView.sd_setImage(with: URL(string: imageUrl), placeholderImage: UIImage.imgPlaceHolder) - contentImageView.loadImage(imageUrl) - let model = CommentImageModel() - model.uploadUrl = imageUrl - uploadModel = model - uploadState = .success - reloadViews() - } - - func givtStateOutsize(_ state: CommentUploadState) { - uploadState = state - } - - func setupCornerRadius(_ radius: CGFloat) { - layer.cornerRadius = radius - indicatorBGView.layer.cornerRadius = radius - } - - // MARK: - Actions - - @objc private func retryButtonAction() { - uploadState = .loading - reloadViews() - if let tapRetryAction = tapRetryAction { - tapRetryAction(self) - } else { - if let img = uploadModel?.image{ - uploadModel?.startUpload(with: img) - }else{ - fatalError() - } - } - } - - @objc private func errorButtonAction() { - if let optionErrorMsg = optionErrorMsg, !optionErrorMsg.isEmpty { - //Hud.showTip(optionErrorMsg) - Hud.toast(str: optionErrorMsg) - } else { - Hud.toast(str: "NSFW materials have been detected, please upload other photos") - } - } - - @objc private func closeButtonAction() { - closeAction?(self) - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/EPHighlightBorderButton.swift b/crush/Crush/Src/Components/UI/Buttons/EPHighlightBorderButton.swift deleted file mode 100644 index ca56869..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/EPHighlightBorderButton.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// EPHighlightBorderButton.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit - -class EPHighlightBorderButton: UIButton { - // MARK: - Initialization - override init(frame: CGRect) { - super.init(frame: frame) - layer.borderColor = UIColor.c.cpvp.cgColor - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Overrides - override var isHighlighted: Bool { - didSet { - layer.borderWidth = isHighlighted ? CLSystemToken.border(token: .bs) : 0 - } - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/EPRadioButton.swift b/crush/Crush/Src/Components/UI/Buttons/EPRadioButton.swift deleted file mode 100644 index 7a5873c..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/EPRadioButton.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// EPRadioButton.swift -// Crush -// -// Created by Leon on 2025/7/25. -// - -import UIKit - -enum EPRadioButtonStyle { - /// 圆圈中带勾,未选中时也带勾 - case checkIn - /// 空圆圈 + 带勾圆圈 - case checkEmpty -} - -class EPRadioButton: CLButton { - - // MARK: - Initialization - override init(frame: CGRect) { - super.init(frame: frame) - contentMode = .center - setContentCompressionResistancePriority(.init(760), for: .horizontal) - setContentCompressionResistancePriority(.init(760), for: .vertical) - setupStyle(.round) - touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - } - - - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Public Methods - /// 标准:比较通用的圆或方形 - func setupStyle(_ radius: IconButtonRadius) { - switch radius { - case .round: - setImage(nil, for: .normal) - setImage(UIImage(named: "checkmark_tick"), for: .selected) - - case .rectangle: - // ⚠️这两张图片没有 - setImage(UIImage(named: "gamer_game_m_unselected"), for: .normal) - setImage(UIImage(named: "gamer_game_m_selected"), for: .selected) - touchAreaInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) - default: - break - } - } - /// 高级:特别模式,样式不同,用在不同地方(一般是定制) - func setupRadioStyle(_ style: EPRadioButtonStyle) { - switch style { - case .checkIn: - let normalImage = MWIconFont.image(fromIcon: .select, size: CGSize(width: 14.4, height: 14.4), color: .c.ctpd) - let selectedImage = MWIconFont.image(fromIcon: .select, size: CGSize(width: 14.4, height: 14.4), color: .c.ctpn) - setImage(normalImage, for: .normal) - setImage(selectedImage, for: .selected) - - let normalBg = UIImage.circleImage(withColor: .c.csed, bounds: CGRect(x: 0, y: 0, width: 24, height: 24)) - let selectedBg = UIImage.circleImage(withColor: .c.cpn, bounds: CGRect(x: 0, y: 0, width: 24, height: 24)) - setBackgroundImage(normalBg, for: .normal) - setBackgroundImage(selectedBg, for: .selected) - case .checkEmpty: - setImage(UIImage(named: "checkmark_tick_light"), for: .normal) - setImage(UIImage(named: "checkmark_tick"), for: .selected) - } - } - - override var isSelected: Bool { - didSet { - super.isSelected = isSelected - } - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/IconButtons.swift b/crush/Crush/Src/Components/UI/Buttons/IconButtons.swift deleted file mode 100644 index 0752e11..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/IconButtons.swift +++ /dev/null @@ -1,491 +0,0 @@ -// -// IconButtons.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -import UIKit - -enum IconButtonStyle: UInt { - case primary - case secondary - case tertiary - case ghost - case tertiaryRoundDark - case tertiaryRoundLight -} - -enum IconSize: Int { - /// 24->21 - case large = 21 - /// 20 - case medium = 20 - /// 16 - case small = 16 - /// 12 - case xs = 12 - /// 10 - case xs10 = 10 - /// 8 - case xxs = 8 -} - -enum IconButtonRadius: UInt { - case none - case rectangle - case round -} - -// Base class: Typically use subclasses below -class EPIconButton: CLButton { - var style: IconButtonStyle - var iconSize: IconSize - var iconCode: IconCode - var radius: IconButtonRadius - - init(style: IconButtonStyle, radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.style = style - self.iconSize = iconSize - self.iconCode = iconCode - self.radius = radius - super.init(frame: .zero) - - clipsToBounds = true - setupRadius() - setupTapExpand() - setupTheme() - } - - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .primary, radius: radius, iconSize: iconSize, iconCode: iconCode) - } - - @available(*, unavailable) - override init(frame: CGRect) { - fatalError("init(frame:) has not been implemented") - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.iconSize = iconSize - self.iconCode = iconCode - self.radius = radius - setupRadius() - setupTapExpand() - setupTheme() - } - - var iconCodeValue: IconCode { - get { iconCode } - set { - iconCode = newValue - setupTheme() - } - } - - func setupTheme() { - // Override in subclasses - } - - func setupRadius() { - switch radius { - case .rectangle: - layer.cornerRadius = 8 - case .round: - layer.cornerRadius = bgImageSize().height * 0.5 - case .none: - layer.cornerRadius = 0 - break - } - } - - func setupTapExpand() { - let bgImageSize = bgImageSize() - let expand = 48 - bgImageSize.width - touchAreaInsets = UIEdgeInsets(top: expand, left: expand, bottom: expand, right: expand) - } - - func bgImageSize() -> CGSize { - switch iconSize { - case .large: return CGSize(width: 48, height: 48) - case .medium: return CGSize(width: 36, height: 36) - case .small: return CGSize(width: 32, height: 32) - case .xs: return CGSize(width: 24, height: 24) - case .xs10: return CGSize(width: 20, height: 20) - case .xxs: return CGSize(width: 16, height: 16) - } - } - - func makeIcon(color: UIColor) -> UIImage { - return MWIconFont.image(fromIconInt: iconCode.rawValue, size: CGSize(width: iconSize.rawValue, height: iconSize.rawValue), color: color, edgeInsets: .zero)! - } - - func makeBGImage(colors: UIColor) -> UIImage { - let bgImageSize = bgImageSize() - return UIImage.withColor(color: colors, size: bgImageSize) - } - - func makeBGCircleBGImage(color: UIColor) -> UIImage { - let bgImageSize = bgImageSize() - let bounds = CGRect(origin: .zero, size: bgImageSize) - return UIImage.circleImage(withColor: color, bounds: bounds)! - } - - func makeBGGradientImage(token: EPGradient) -> UIImage { - var colors: [UIColor] = [] - if let firstColor = token.firstColor { - colors.append(firstColor) - } - if let secondColor = token.secondColor { - colors.append(secondColor) - } - let bgImageSize = bgImageSize() - return UIImage.gradientHImageWithSize(size: bgImageSize, colors: colors) - } - - func setupTapExpandEdge(insets: UIEdgeInsets) { - touchAreaInsets = insets - } -} - -// MARK: - EPIconPrimaryButton - -class EPIconPrimaryButton: EPIconButton { - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .primary, radius: radius, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - let normalColor = UIColor.c.ctpn // EPSystemToken.color(.txtPrimarySpecialmapNormal) - let disabledColor = UIColor.c.ctpd // EPSystemToken.color(.txtPrimaryDisabled) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: normalColor), for: .highlighted) - setImage(makeIcon(color: disabledColor), for: .disabled) - - let normalGradient = CLSystemToken.gradient(token: .cpgn) // EPSystemToken.gradient(.primaryGradientNormal) - let pressGradient = CLSystemToken.gradient(token: .cpgp) // EPSystemToken.gradient(.primaryGradientPress) - let disabledGradient = CLSystemToken.gradient(token: .csed) // EPSystemToken.gradient(.surfaceElementDisabled) - - setBackgroundImage(makeBGGradientImage(token: normalGradient), for: .normal) - setBackgroundImage(makeBGGradientImage(token: pressGradient), for: .highlighted) - setBackgroundImage(makeBGGradientImage(token: disabledGradient), for: .disabled) - } -} - -// MARK: - EPIconSecondaryButton - -class EPIconSecondaryButton: EPIconButton { - convenience init(iconSize: IconSize, iconCode: IconCode) { - self.init(style: .secondary, radius: .none, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - layer.borderColor = UIColor.c.cpvn.cgColor // EPSystemToken.color(.primaryVariantNormal).cgColor - - let normalColor: UIColor = .c.cpvn // EPSystemToken.color(.primaryVariantNormal) - let disabledColor: UIColor = .c.cpvd // EPSystemToken.color(.primaryVariantDisabled) - let pressColor: UIColor = .c.cpvp // EPSystemToken.color(.primaryVariantPress) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: pressColor), for: .highlighted) - } - - override func bgImageSize() -> CGSize { - switch iconSize { - case .large: return CGSize(width: 48, height: 48) - case .medium: return CGSize(width: 36, height: 36) - case .small: return CGSize(width: 32, height: 32) - case .xs: return CGSize(width: 24, height: 24) - default: return .zero - } - } -} - -// MARK: - EPIconTertiaryButton - -class EPIconTertiaryButton: EPIconButton { - var optionalIconColor: UIColor? { - didSet { - if let color = optionalIconColor { - setImage(makeIcon(color: color), for: .normal) - setImage(makeIcon(color: color), for: .highlighted) - setImage(makeIcon(color: color), for: .selected) - } - } - } - - var optionalBackgroundColor: UIColor? { - didSet { - if let color = optionalBackgroundColor { - setBackgroundImage(makeBGImage(colors: color), for: .normal) - setBackgroundImage(makeBGImage(colors: color), for: .disabled) - setBackgroundImage(makeBGImage(colors: color), for: .highlighted) - } - } - } - - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .primary, radius: radius, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - let normalColor: UIColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - let disabledColor: UIColor = .c.ctd // EPSystemToken.color(.txtDisabled) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - setImage(makeIcon(color: normalColor), for: .selected) - - let normalBgColor: UIColor = .c.csen // EPSystemToken.color(.surfaceElementNormal) - let disabledBgColor: UIColor = .c.csed // EPSystemToken.color(.surfaceElementDisabled) - let pressBgColor: UIColor = .c.csep // EPSystemToken.color(.surfaceElementPress) - - setBackgroundImage(makeBGImage(colors: normalBgColor), for: .normal) - setBackgroundImage(makeBGImage(colors: disabledBgColor), for: .disabled) - setBackgroundImage(makeBGImage(colors: pressBgColor), for: .highlighted) - } - - func clearBackgroundImage() { - setBackgroundImage(nil, for: .normal) - } - - func resumeBackgroundImage() { - let normalBgColor: UIColor = .c.csen // EPSystemToken.color(.surfaceElementNormal) - setBackgroundImage(makeBGImage(colors: normalBgColor), for: .normal) - } -} - -// MARK: - EPIconGhostButton - -class EPIconGhostButton: EPIconButton { - var optionalIconColor: UIColor? { - didSet { - if let color = optionalIconColor { - setImage(makeIcon(color: color), for: .normal) - setImage(makeIcon(color: color), for: .highlighted) - setImage(makeIcon(color: color), for: .selected) - } - } - } - - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .ghost, radius: radius, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - let normalColor: UIColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - let disabledColor: UIColor = .c.ctd // EPSystemToken.color(.txtDisabled) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - - let pressBgColor: UIColor = .c.csep // EPSystemToken.color(.surfaceElementPress) - - setBackgroundImage(nil, for: .normal) - setBackgroundImage(nil, for: .disabled) - setBackgroundImage(makeBGCircleBGImage(color: pressBgColor), for: .highlighted) - } -} - -// MARK: - EPIconGhostSecondaryButton - -class EPIconGhostSecondaryButton: EPIconButton { - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .ghost, radius: .round, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - let normalColor: UIColor = .c.ctsn // EPSystemToken.color(.txtSecondaryNormal) - let disabledColor: UIColor = .c.ctsd // EPSystemToken.color(.txtSecondaryDisabled) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - - let pressBgColor: UIColor = .c.csep // EPSystemToken.color(.surfaceElementPress) - - setBackgroundImage(nil, for: .normal) - setBackgroundImage(nil, for: .disabled) - setBackgroundImage(makeBGCircleBGImage(color: pressBgColor), for: .highlighted) - } -} - -// MARK: - EPIconContrastTertiaryButton - -class EPIconContrastTertiaryButton: EPIconButton { - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .ghost, radius: .round, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - let normalColor: UIColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - let disabledColor: UIColor = .c.ctd // EPSystemToken.color(.txtDisabled) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - setImage(makeIcon(color: normalColor), for: .selected) - - let normalBgColor: UIColor = .c.csedn // EPSystemToken.color(.surfaceElementDarkNormal) - let disabledBgColor: UIColor = .c.csedd // EPSystemToken.color(.surfaceElementDarkDisabled) - let pressBgColor: UIColor = .c.csedp // EPSystemToken.color(.surfaceElementDarkPress) - - setBackgroundImage(makeBGImage(colors: normalBgColor), for: .normal) - setBackgroundImage(makeBGImage(colors: disabledBgColor), for: .disabled) - setBackgroundImage(makeBGImage(colors: pressBgColor), for: .highlighted) - } -} - -// MARK: - EPFloatingButton - -class EPFloatingButton: EPIconButton { - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .ghost, radius: .round, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - setupNormalMode() - } - - func setupNormalMode() { - let normalColor: UIColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - let disabledColor: UIColor = .c.ctpsd // EPSystemToken.color(.txtPrimarySpecialmapDisable) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - - let pressGradient = CLSystemToken.gradient(token: .cpgp) // EPSystemToken.gradient(.primaryGradientPress) - let gradient = CLSystemToken.gradient(token: .cpgn) // EPSystemToken.gradient(.primaryGradientNormal) - let disabledBgColor = UIColor.c.csfn // EPSystemToken.color(.surfaceFloatNormal) - - setBackgroundImage(gradient.toImage(size: CGSize(width: 10, height: 10)), for: .normal) - setBackgroundImage(makeBGImage(colors: disabledBgColor), for: .disabled) - setBackgroundImage(pressGradient.toImage(size: CGSize(width: 10, height: 10)), for: .highlighted) - - layer.removeAllAnimations() - } - - func setupMatchingState() { - let normalColor: UIColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - let disabledColor: UIColor = .c.ctd // EPSystemToken.color(.txtDisabled) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - - let pressGradient = CLSystemToken.gradient(token: .cpgp) // EPSystemToken.gradient(.positiveGradientPress) - let gradient = CLSystemToken.gradient(token: .cpgn) // EPSystemToken.gradient(.positiveGradientNormal) - let disabledBgColor: UIColor = .c.csfn // EPSystemToken.color(.surfaceFloatNormal) - - setBackgroundImage(gradient.toImage(size: CGSize(width: 10, height: 10)), for: .normal) - setBackgroundImage(makeBGImage(colors: disabledBgColor), for: .disabled) - setBackgroundImage(pressGradient.toImage(size: CGSize(width: 10, height: 10)), for: .highlighted) - - let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - rotationAnimation.toValue = NSNumber(value: Double.pi * 2.0) - rotationAnimation.duration = 1.5 - rotationAnimation.isRemovedOnCompletion = false - rotationAnimation.isCumulative = true - rotationAnimation.repeatCount = .infinity - layer.add(rotationAnimation, forKey: "rotationAnimation") - } - - override func layoutSubviews() { - super.layoutSubviews() - if radius == .round { - layer.cornerRadius = bounds.size.height * 0.5 - } - } - - deinit { - layer.removeAllAnimations() - } -} - -// MARK: - EPIconTertiaryDarkButton - -class EPIconTertiaryDarkButton: EPIconButton { - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .tertiaryRoundDark, radius: radius, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - let normalColor: UIColor = .c.ctpsn // EPSystemToken.color(.txtPrimarySpecialmapNormal) - let disabledColor: UIColor = .c.ctpd // EPSystemToken.color(.txtPrimaryDisabled) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - - let normalBgColor: UIColor = .c.csedn // EPSystemToken.color(.surfaceElementDarkNormal) - let disabledBgColor: UIColor = .c.csedd // EPSystemToken.color(.surfaceElementDarkDisabled) - let pressBgColor: UIColor = .c.csedp // EPSystemToken.color(.surfaceElementDarkPress) - - setBackgroundImage(makeBGImage(colors: normalBgColor), for: .normal) - setBackgroundImage(makeBGImage(colors: disabledBgColor), for: .disabled) - setBackgroundImage(makeBGImage(colors: pressBgColor), for: .highlighted) - } - - func clearBackgroundImage() { - setBackgroundImage(nil, for: .normal) - } - - func resumeBackgroundImage() { - let normalBgColor:UIColor = .c.csedn//EPSystemToken.color(.surfaceElementDarkNormal) - setBackgroundImage(makeBGImage(colors: normalBgColor), for: .normal) - } -} - -// MARK: - EPIconTertiaryLightButton - -class EPIconTertiaryLightButton: EPIconButton { - convenience init(radius: IconButtonRadius, iconSize: IconSize, iconCode: IconCode) { - self.init(style: .tertiaryRoundLight, radius: radius, iconSize: iconSize, iconCode: iconCode) - } - - override func setupTheme() { - let normalColor: UIColor = .c.ctpsn//EPSystemToken.color(.txtPrimarySpecialmapNormal) - let disabledColor: UIColor = .c.ctpsd//EPSystemToken.color(.txtPrimarySpecialmapDisable) - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - - let normalBgColor: UIColor = .c.cseln//EPSystemToken.color(.surfaceElementLightNormal) - let disabledBgColor: UIColor = .c.cseld//EPSystemToken.color(.surfaceElementLightDisabled) - let pressBgColor: UIColor = .c.cselp//EPSystemToken.color(.surfaceElementLightPress) - - setBackgroundImage(makeBGImage(colors: normalBgColor), for: .normal) - setBackgroundImage(makeBGImage(colors: disabledBgColor), for: .disabled) - setBackgroundImage(makeBGImage(colors: pressBgColor), for: .highlighted) - } -} - -class EPIconDestructiveButton : EPIconButton{ - override func setupTheme() { - let normalColor: UIColor = .c.ctpn - let disabledColor: UIColor = .c.ctpd - - setImage(makeIcon(color: normalColor), for: .normal) - setImage(makeIcon(color: disabledColor), for: .disabled) - setImage(makeIcon(color: normalColor), for: .highlighted) - - let normalBgColor: UIColor = .c.cin - let disabledBgColor: UIColor = .c.cid - let pressBgColor: UIColor = .c.cip - - setBackgroundImage(makeBGImage(colors: normalBgColor), for: .normal) - setBackgroundImage(makeBGImage(colors: disabledBgColor), for: .disabled) - setBackgroundImage(makeBGImage(colors: pressBgColor), for: .highlighted) - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/InDecreaseButton.swift b/crush/Crush/Src/Components/UI/Buttons/InDecreaseButton.swift deleted file mode 100644 index 9055631..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/InDecreaseButton.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// Create.swift -// Crush -// -// Created by Leon on 2025/7/23. -// - -import SnapKit -import UIKit - -/// A button for incrementing or decrementing values, used in order placement or tipping amount/unit adjustments -class InDecreaseButton: UIButton { - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - setupButton() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupButton() - } - - private func setupButton() { - isEnabled = true - layer.cornerRadius = 8 - layer.masksToBounds = true - - // Assuming a fixed size of 48x48 as per original code - snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 48, height: 48)) - } - } - - // MARK: - Public Methods - - func setupIncreaseState() { - // Assuming MWIconFont is a utility for generating icon images - // IconCodeAdd is likely an enum or constant for the "+" icon - let iconImage = MWIconFont.image(fromIcon: .add, size: CGSize(width: 20, height: 20), color: .white) - setImage(iconImage, for: .normal) - } - - func setupDecreaseState() { - // Assuming IconCodeReduce is an enum or constant for the "−" icon - let iconImage = MWIconFont.image(fromIcon: .reduce, size: CGSize(width: 20, height: 20), color: .white) - setImage(iconImage, for: .normal) - } - - // MARK: - Property Overrides - - override var isEnabled: Bool { - didSet { - tintColor = isEnabled ? UIColor.text : UIColor.disable - - backgroundColor = isEnabled ? UIColor.c.csen : UIColor.c.csed - } - } -} - -// MARK: - Placeholder Assumptions - -// Assuming MWIconFont is a utility class for generating icon images -enum InDecreaseIconCode { - case add - case reduce -} diff --git a/crush/Crush/Src/Components/UI/Buttons/StyleButton.swift b/crush/Crush/Src/Components/UI/Buttons/StyleButton.swift deleted file mode 100644 index 844eddd..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/StyleButton.swift +++ /dev/null @@ -1,362 +0,0 @@ -// -// StyleButton.swift -// Crush -// -// Created by Leon on 2025/7/17. -// - -import SwiftyAttributes -import UIKit - -enum StyleButtonSize { - /// height: 32, tls - case small - /// height: 40, tlm - case medium - /// height: 48, tll - case large -} - -class StyleButton: CLButton { - var radius: CGFloat = 0 - private var enableBorderColor: UIColor? - private var disableBorderColor: UIColor? - private var pressBorderColor: UIColor? - - var text: String? { - didSet { - setTitle(text, for: .normal) - } - } - - var titleColor: UIColor? { - didSet { - setTitleColor(titleColor, for: .normal) - } - } - - override var isEnabled: Bool { - didSet { - if let enableColor = enableBorderColor, let disableColor = disableBorderColor { - layer.borderColor = isEnabled ? enableColor.cgColor : disableColor.cgColor - } - } - } - - override var isHighlighted: Bool { - didSet { - if let enableColor = enableBorderColor, let pressColor = pressBorderColor { - layer.borderColor = isHighlighted ? pressColor.cgColor : enableColor.cgColor - } - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = nil - tintColor = nil - contentMode = .scaleAspectFill - titleLabel?.setContentHuggingPriority(UILayoutPriority(248), for: .horizontal) - //setContentHuggingPriority(UILayoutPriority(248), for: .horizontal) - setContentCompressionResistancePriority(UILayoutPriority(755), for: .horizontal) - titleLabel?.adjustsFontSizeToFitWidth = true - - constraints.forEach { - if $0.firstAttribute == .height { - removeConstraint($0) - } - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Style Buttons - public func primary(size: StyleButtonSize) { - setTitleColor(.c.ctpn, for: .normal) - setTitleColor(.c.ctpd, for: .disabled) - setTitleColor(.c.ctpn, for: .highlighted) - -// var configuration = UIButton.Configuration.plain() -// configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 32, bottom: 0, trailing: 32) -// self.configuration = configuration - - // 设置状态更新处理 -// configurationUpdateHandler = { [weak self] button in -// guard self != nil else { return } -// var updatedConfig = button.configuration ?? UIButton.Configuration.plain() -// -// // 根据按钮状态设置不同的背景图片 -// switch button.state { -// case .normal: -// let gradient = CLSystemToken.gradient(token: .cpgn) -// let normalBg = gradient.toImage(size: .init(width: 40, height: 10)) -// updatedConfig.background.image = normalBg -// updatedConfig.background.imageContentMode = .scaleAspectFill -// case .highlighted: -// let highlight = CLSystemToken.gradient(token: .cpgp) -// let pressBg = highlight.toImage(size: .init(width: 40, height: 10)) -// updatedConfig.background.image = pressBg -// updatedConfig.background.imageContentMode = .scaleAspectFill -// case .disabled: -// let disabledImage = UIImage.withColor(color: .c.cpd, size: .init(width: 20, height: 20)) -// updatedConfig.background.image = disabledImage -// updatedConfig.background.imageContentMode = .scaleAspectFill -// default: -// let gradient = CLSystemToken.gradient(token: .cpgn) -// let normalBg = gradient.toImage(size: .init(width: 40, height: 10)) -// updatedConfig.background.image = normalBg -// } -// -// button.configuration = updatedConfig -// } - - let gradient = CLSystemToken.gradient(token: .cpgn) - let normalBg = gradient.toImage(size: .init(width: 40, height: 10)) - setBackgroundImage(normalBg, for: .normal) - - let highlight = CLSystemToken.gradient(token: .cpgp) - let pressBg = highlight.toImage(size: .init(width: 40, height: 10)) - setBackgroundImage(pressBg, for: .highlighted) - - let disabledImage = UIImage.withColor(color: .c.cpd, size: .init(width: 20, height: 20)) - setBackgroundImage(disabledImage, for: .disabled) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - layoutIfNeeded() - } - - public func tertiary(size: StyleButtonSize) { - setTitleColor(.c.ctpn, for: .normal) - setTitleColor(.c.ctpd, for: .disabled) - setTitleColor(.c.ctpn, for: .highlighted) - - let normalBg = UIImage.withColor(color: .c.csen, size: .init(width: 10, height: 10)) - setBackgroundImage(normalBg, for: .normal) - let pressBg = UIImage.withColor(color: .c.csep, size: .init(width: 10, height: 10)) - setBackgroundImage(pressBg, for: .highlighted) - let disabledImage = UIImage.withColor(color: .c.csed, size: .init(width: 10, height: 10)) - setBackgroundImage(disabledImage, for: .disabled) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - setNeedsDisplay() - layoutIfNeeded() - } - - public func defaultSecondary(size: StyleButtonSize) { - setTitleColor(.c.cpvn, for: .normal) - setTitleColor(.c.ctd, for: .disabled) - setTitleColor(.c.cpvp, for: .highlighted) - - enableBorderColor = .c.cpvn - disableBorderColor = .c.cpvd - pressBorderColor = .c.cpvp - - layer.borderWidth = CLSystemToken.border(token: .bs) - isEnabled = true - radius = CLSystemToken.radius(token: .rp) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - layoutIfNeeded() - } - - public func defaultDestructive(size: StyleButtonSize) { - setTitleColor(.c.ctpn, for: .normal) - setTitleColor(.c.ctpd, for: .disabled) - setTitleColor(.c.ctpn, for: .highlighted) - - // iOS 15+设置方法 -// var configuration = UIButton.Configuration.plain() -// configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 32, bottom: 0, trailing: 32) -// self.configuration = configuration -// -// // 设置状态更新处理 -// configurationUpdateHandler = { [weak self] button in -// guard self != nil else { return } -// var updatedConfig = button.configuration ?? UIButton.Configuration.plain() -// -// // 根据按钮状态设置不同的背景图片 -// switch button.state { -// case .normal: -// let normalBg = UIImage.withColor(color: .c.cin, size: .init(width: 10, height: 10)) -// updatedConfig.background.image = normalBg -// updatedConfig.background.imageContentMode = .scaleAspectFill -// case .highlighted: -// let pressBg = UIImage.withColor(color: .c.cip, size: .init(width: 10, height: 10)) -// updatedConfig.background.image = pressBg -// updatedConfig.background.imageContentMode = .scaleAspectFill -// case .disabled: -// let disabledImage = UIImage.withColor(color: .c.cid, size: .init(width: 10, height: 10)) -// updatedConfig.background.image = disabledImage -// updatedConfig.background.imageContentMode = .scaleAspectFill -// default: -// let gradient = CLSystemToken.gradient(token: .cin) -// let normalBg = gradient.toImage(size: .init(width: 40, height: 10)) -// updatedConfig.background.image = normalBg -// } -// -// button.configuration = updatedConfig -// } - let normalBg = UIImage.withColor(color: .c.cin, size: .init(width: 10, height: 10)) - setBackgroundImage(normalBg, for: .normal) - - let pressBg = UIImage.withColor(color: .c.cip, size: .init(width: 10, height: 10)) - setBackgroundImage(pressBg, for: .highlighted) - - let disabledImage = UIImage.withColor(color: .c.cid, size: .init(width: 10, height: 10)) - setBackgroundImage(disabledImage, for: .disabled) - - commonDealSize(size: size) - commonDealSpacing(size: size) - layoutIfNeeded() - } - - /// 渐变主题色 button - public func context(size: StyleButtonSize) { - setTitleColor(.c.csbn, for: .normal) - setTitleColor(.c.ctpd, for: .disabled) - setTitleColor(.c.csbp, for: .highlighted) - - // let c = UIColor.c.ccvn - let gradient = CLSystemToken.gradient(token: .ccvn) - let normalBg = gradient.toImage(size: CGSize(width: 40, height: 10)) - setBackgroundImage(normalBg, for: .normal) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - layoutIfNeeded() - } - - public func vip(size: StyleButtonSize){ - setTitleColor(.c.csbn, for: .normal) - setTitleColor(.c.ctpd, for: .disabled) - setTitleColor(.c.csbp, for: .highlighted) - - let gradient = CLSystemToken.gradient(token: .ccvn) - let normalBg = gradient.toImage(size: CGSize(width: 40, height: 10)) - setBackgroundImage(normalBg, for: .normal) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - layoutIfNeeded() - } - - public func text(size: StyleButtonSize){ - setTitleColor(.c.cpvn, for: .normal) - setTitleColor(.c.ctd, for: .disabled) - setTitleColor(.c.cpvp, for: .highlighted) - - setBackgroundImage(nil, for: .normal) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - layoutIfNeeded() - } - - public func contrastTertiaryLight(size: StyleButtonSize){ - setTitleColor(.c.ctpn, for: .normal) - setTitleColor(.c.ctpd, for: .disabled) - setTitleColor(.c.ctpn, for: .highlighted) - - let normalBg = UIImage.withColor(color: .c.cseln, size: .init(width: 10, height: 10)) - setBackgroundImage(normalBg, for: .normal) - let pressBg = UIImage.withColor(color: .c.cselp, size: .init(width: 10, height: 10)) - setBackgroundImage(pressBg, for: .highlighted) - let disabledImage = UIImage.withColor(color: .c.cseld, size: .init(width: 10, height: 10)) - setBackgroundImage(disabledImage, for: .disabled) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - setNeedsDisplay() - layoutIfNeeded() - } - - public func contrastTertiaryDark(size: StyleButtonSize){ - setTitleColor(.c.ctpn, for: .normal) - setTitleColor(.c.ctpd, for: .disabled) - setTitleColor(.c.ctpn, for: .highlighted) - - let normalBg = UIImage.withColor(color: .c.csedn, size: .init(width: 10, height: 10)) - setBackgroundImage(normalBg, for: .normal) - let pressBg = UIImage.withColor(color: .c.csedp, size: .init(width: 10, height: 10)) - setBackgroundImage(pressBg, for: .highlighted) - let disabledImage = UIImage.withColor(color: .c.csedd, size: .init(width: 10, height: 10)) - setBackgroundImage(disabledImage, for: .disabled) - - commonDealSize(size: size) - commonDealSpacing(size: size) - - setNeedsDisplay() - layoutIfNeeded() - } - - - // MARK: - Common - - private func commonDealSize(size: StyleButtonSize) { - switch size { - case .small: - let typography = CLSystemToken.typography(token: .tls) - titleLabel?.font = typography.font - snp.makeConstraints { make in - make.height.equalTo(32) - } - case .medium: - let typography = CLSystemToken.typography(token: .tlm) - titleLabel?.font = typography.font - snp.makeConstraints { make in - make.height.equalTo(40) - } - case .large: - let typography = CLSystemToken.typography(token: .tll) - titleLabel?.font = typography.font - snp.makeConstraints { make in - make.height.equalTo(48) - } - } - } - - private func commonDealSpacing(size: StyleButtonSize) { - switch size { - case .small: - contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) -// var configuration = self.configuration ?? UIButton.Configuration.plain() -// configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16) -// self.configuration = configuration - case .medium: - contentEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) -// var configuration = self.configuration ?? UIButton.Configuration.plain() -// configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20) -// self.configuration = configuration - case .large: - contentEdgeInsets = UIEdgeInsets(top: 0, left: 32, bottom: 0, right: 32) -// var configuration = self.configuration ?? UIButton.Configuration.plain() -// configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 32, bottom: 0, trailing: 32) -// self.configuration = configuration - } - } - - // MARK: layout - - override func layoutSubviews() { - super.layoutSubviews() - if radius >= 1 { - layer.cornerRadius = radius - } else { - layer.cornerRadius = bounds.size.height * 0.5 - } - layer.masksToBounds = true - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/StyleButtonExt.swift b/crush/Crush/Src/Components/UI/Buttons/StyleButtonExt.swift deleted file mode 100644 index 8e7fe74..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/StyleButtonExt.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// StyleButtonExt.swift -// Crush -// -// Created by Leon on 2025/8/26. -// - -extension StyleButton{ - /// Coin : price * 100 - static func getUnlockAttributeTitleByCoin(coin: Int, string: String = "") -> NSAttributedString { - let coin = Coin(cents: coin) - - let text = " \(coin.thousandthsFormatted) \(string)" - let attributedString = NSMutableAttributedString() - if let iconImage = UIImage(named: "icon_32_diamond") { // 替换为你的图标名称 - let attachment = NSTextAttachment() - attachment.image = iconImage - - let iconSize = CGSize(width: 24, height: 24) // 调整为你需要的大小 - attachment.bounds = CGRect(origin: .init(x: 0, y: -4), size: iconSize) - - let iconAttributedString = NSAttributedString(attachment: attachment) - attributedString.append(iconAttributedString) - } - - // 添加文字并设置样式 - let textAttributedString = text.withAttributes([ - .font(.t.tll), // 设置字体 - .textColor(.white), - ]) - attributedString.append(textAttributedString) - return attributedString - } - - /// 黑色vip + 文字 AttributeString - static func getVIPBlackTitleByWords(words: String) -> NSAttributedString { - -// let text = " \(words)" -// let attributedString = NSMutableAttributedString() -// if let iconImage = MWIconFont.image(fromIcon: .iconVip, size: CGSize(width: 24, height: 24), color: .black) { // 替换为你的图标名称 -// let attachment = NSTextAttachment() -// attachment.image = iconImage -// -// let iconSize = CGSize(width: 24, height: 24) // 调整为你需要的大小 -// attachment.bounds = CGRect(origin: .init(x: 0, y: -4), size: iconSize) -// -// let iconAttributedString = NSAttributedString(attachment: attachment) -// attributedString.append(iconAttributedString) -// } -// -// // 添加文字并设置样式 -// let textAttributedString = text.withAttributes([ -// .font(.t.tbsl), // 设置字体 -// .textColor(.black), -// ]) -// attributedString.append(textAttributedString) -// return attributedString - return NSAttributedString.getIconTitleAttributeByWords(words: words, icon:.iconVip, textFont: .t.tll) - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/TextButton.swift b/crush/Crush/Src/Components/UI/Buttons/TextButton.swift deleted file mode 100644 index 275740f..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/TextButton.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// TextButton.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit - -/// Default: tll, color: cpvn -class TextButton: CLButton { - - var horizontalPadding: CGFloat = 0.0 { // Default text horizontal padding - didSet { - updateTitleEdgeInsets() - } - } - var text:String? { - didSet { - setTitle(text, for: .normal) - } - } - var titleColor:UIColor? { - didSet { - setTitleColor(titleColor, for: .normal) - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - // Assuming .t.tll and .c.tpvn are defined font and color constants - titleLabel?.font = .t.tll - setTitleColor(.c.cpvn, for: .normal) - setTitleColor(.c.ctd, for: .disabled) - setTitleColor(.c.cpvp, for: .highlighted) - - // Ensure titleLabel uses auto layout - titleLabel?.translatesAutoresizingMaskIntoConstraints = false - - // Basic styling - backgroundColor = .clear // Optional: set as needed - layer.cornerRadius = 8 // Optional: for rounded corners - clipsToBounds = true - - // Apply initial padding - updateTitleEdgeInsets() - } - - private func updateTitleEdgeInsets() { - // Adjust text padding - contentEdgeInsets = UIEdgeInsets(top: 0, left: horizontalPadding, bottom: 0, right: horizontalPadding) - } - -// override var intrinsicContentSize: CGSize { -// // Calculate intrinsic size with custom height -// let superSize = super.intrinsicContentSize -// return CGSize(width: superSize.width + horizontalPadding * 2, height: buttonHeight) -// } - - // Ensure layout updates when properties change - override func layoutSubviews() { - super.layoutSubviews() - updateTitleEdgeInsets() - } -} diff --git a/crush/Crush/Src/Components/UI/Buttons/TickButton.swift b/crush/Crush/Src/Components/UI/Buttons/TickButton.swift deleted file mode 100644 index 99cc432..0000000 --- a/crush/Crush/Src/Components/UI/Buttons/TickButton.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// TickButton.swift -// Crush -// -// Created by Leon on 2025/7/19. -// - -import UIKit -import SnapKit - -class TickButton: CLButton { - // MARK: - Properties - private var normalTintColor: UIColor - private var disableTintColor: UIColor - - // MARK: - Initialization - override init(frame: CGRect) { - normalTintColor = .c.ctpn - disableTintColor = .c.ctpd - - super.init(frame: frame) - tickmark() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - func tickmark() { - layer.cornerRadius = 16 - clipsToBounds = true - - let tick = MWIconFont.image(fromIcon: .select, size: CGSize(width: 16, height: 16), color: .c.ctpn) - setImage(tick, for: .normal) - setBackgroundImage(UIImage.withColor(color: .c.cpn, size: CGSize(width: 10, height: 10)), for: .normal) - setBackgroundImage(UIImage.withColor(color: .c.cpd, size: CGSize(width: 10, height: 10)), for: .disabled) - setBackgroundImage(UIImage.withColor(color: .c.cpp, size: CGSize(width: 10, height: 10)), for: .highlighted) - - snp.makeConstraints { make in - make.height.equalTo(32) - make.width.equalTo(52) - } - } - - override var isEnabled: Bool { - didSet { - tintColor = isEnabled ? normalTintColor : disableTintColor - } - } -} diff --git a/crush/Crush/Src/Components/UI/CustomViews/GestureSlider.swift b/crush/Crush/Src/Components/UI/CustomViews/GestureSlider.swift deleted file mode 100644 index b371da5..0000000 --- a/crush/Crush/Src/Components/UI/CustomViews/GestureSlider.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// GestureSlider.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import UIKit - -/// 音频专用滑动条 -class CustomSlider: UIControl { - private let trackView = UIView() - private var tickViews: [UIView] = [] - private let thumbView = UIView() - - var numberOfSteps: Int = 11 { - didSet { - setupTicks() - layoutIfNeeded() - } - } - - @Published var selectedStep: Int = 5 { - didSet { - updateThumbPosition(animated: true) - } - } - - private let trackHeight: CGFloat = 20 - private let tickWidth: CGFloat = 2 - private let tickHeight: CGFloat = 12 - private let thumbSize = CGSize(width: 8, height: 28) - - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - - private func setup() { - backgroundColor = .clear - - // Track - trackView.backgroundColor = .c.csen - trackView.layer.cornerRadius = 12 - addSubview(trackView) - trackView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - // Thumb - thumbView.backgroundColor = .c.cpvn - thumbView.layer.cornerRadius = thumbSize.width / 2 - addSubview(thumbView) - - // Gesture - let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) - addGestureRecognizer(pan) - - setupTicks() - - } - - private func setupTicks() { - tickViews.forEach { $0.removeFromSuperview() } - tickViews.removeAll() - - for _ in 0 ..< numberOfSteps { - let tick = UIView() - tick.backgroundColor = UIColor(white: 1, alpha: 0.3) - addSubview(tick) - tickViews.append(tick) - } - - bringSubviewToFront(thumbView) - } - - override func layoutSubviews() { - super.layoutSubviews() - - // Layout ticks - let lrPadding = 24.0 - let ticksWitdh = bounds.width - lrPadding * 2 - let spacing = ticksWitdh / CGFloat(numberOfSteps - 1) - for (index, tick) in tickViews.enumerated() { - let x = CGFloat(index) * spacing + lrPadding // 24 is leftPadding - tick.frame = CGRect( - x: x - tickWidth / 2, - y: (bounds.height - tickHeight) / 2, - width: tickWidth, - height: tickHeight - ) - } - - updateThumbPosition(animated: false) - } - - private func updateThumbPosition(animated: Bool) { - guard numberOfSteps > 1 else { return } - let lrPadding = 24.0 - let ticksWitdh = bounds.width - lrPadding * 2 - let spacing = ticksWitdh / CGFloat(numberOfSteps - 1) - let x = CGFloat(selectedStep) * spacing - thumbSize.width / 2 + lrPadding - let y = (bounds.height - thumbSize.height) / 2 - - let update = { - self.thumbView.frame = CGRect(origin: CGPoint(x: x, y: y), size: self.thumbSize) - } - - if animated { - UIView.animate(withDuration: 0.2, animations: update) - } else { - update() - } - } - - @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { - let location = gesture.location(in: self) - let stepWidth = bounds.width / CGFloat(numberOfSteps - 1) - var newStep = Int(round(location.x / stepWidth)) - newStep = max(0, min(numberOfSteps - 1, newStep)) - - if selectedStep != newStep { - selectedStep = newStep - sendActions(for: .valueChanged) - } - - if gesture.state == .ended { - updateThumbPosition(animated: true) - } - } -} diff --git a/crush/Crush/Src/Components/UI/CustomViews/HorizontalScrollTagsView.swift b/crush/Crush/Src/Components/UI/CustomViews/HorizontalScrollTagsView.swift deleted file mode 100644 index bde80a1..0000000 --- a/crush/Crush/Src/Components/UI/CustomViews/HorizontalScrollTagsView.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// HorizontalScrollTagsView.swift -// Crush -// -// Created by Leon on 2025/9/28. -// - -import UIKit -import SnapKit - -/* -控件: 横向可滚动的tag选择器,可多选,可不选 -要求: 使用EPChipFilterButton -可能用scrollView +stackView的方式? - -外部可能约束 HorizontalScrollTagsView的高度和宽度。EPChipFilterButton竖直方向上居中,高度默认控件EPChipFilterButton内部自行约束高度。 - - 选择变更,触发string,和index索引的变更回调 - 可设置选择tag区域 左右inset - */ - -/* - Example: - - let tagsView = HorizontalScrollTagsView() - tagsView.setTags(["标签1", "标签2", "标签3"]) - tagsView.selectionCallback = { selectedTags, selectedIndices in - print("选中的标签: \(selectedTags)") - print("选中的索引: \(selectedIndices)") - } - - */ - -/// 标签选择回调 -typealias TagSelectionCallback = ([String], [Int]) -> Void - -class HorizontalScrollTagsView: UIView { - - // MARK: - Properties - - /// 滚动视图 - private var scrollView: UIScrollView! - - /// 标签容器 - private var stackView: UIStackView! - - /// 标签按钮数组 - private var tagButtons: [EPChipFilterButton] = [] - - /// 标签数据源 - private var tags: [String] = [] - - /// 选中的标签索引 - private var selectedIndices: Set = [] - - /// 选择回调 - var selectionCallback: TagSelectionCallback? - - /// 标签间距 - var tagSpacing: CGFloat = 12 { - didSet { - stackView.spacing = tagSpacing - } - } - - /// 左右内边距 - var horizontalInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24) { - didSet { - updateStackViewConstraints() - } - } - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - private func setupViews() { - // 设置滚动视图 - scrollView = UIScrollView() - scrollView.showsHorizontalScrollIndicator = false - scrollView.showsVerticalScrollIndicator = false - scrollView.contentInsetAdjustmentBehavior = .never - addSubview(scrollView) - scrollView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - // 设置堆栈视图 - stackView = UIStackView() - stackView.axis = .horizontal - stackView.alignment = .center - stackView.distribution = .fill - stackView.spacing = tagSpacing - scrollView.addSubview(stackView) - updateStackViewConstraints() - } - - private func updateStackViewConstraints() { - stackView.snp.remakeConstraints { make in - make.edges.equalToSuperview().inset(horizontalInsets) - make.height.equalToSuperview() - } - } - - // MARK: - Public Methods - - /// 设置标签数据 - /// - Parameter tags: 标签文本数组 - func setTags(_ tags: [String]) { - self.tags = tags - createTagButtons() - } - - /// 设置选中的标签索引 - /// - Parameter indices: 选中的索引数组 - func setSelectedIndices(_ indices: [Int]) { - selectedIndices.removeAll() - for index in indices { - if index >= 0 && index < tagButtons.count { - selectedIndices.insert(index) - tagButtons[index].isSelected = true - } - } - } - - /// 获取选中的标签文本 - /// - Returns: 选中的标签文本数组 - func getSelectedTags() -> [String] { - return selectedIndices.compactMap { index in - index < tags.count ? tags[index] : nil - } - } - - /// 获取选中的标签索引 - /// - Returns: 选中的标签索引数组 - func getSelectedIndices() -> [Int] { - return Array(selectedIndices).sorted() - } - - /// 清除所有选择 - func clearSelection() { - selectedIndices.removeAll() - tagButtons.forEach { $0.isSelected = false } - notifySelectionChanged() - } - - // MARK: - Private Methods - - private func createTagButtons() { - // 清除现有按钮 - tagButtons.forEach { $0.removeFromSuperview() } - tagButtons.removeAll() - selectedIndices.removeAll() - - // 创建新按钮 - for (index, tag) in tags.enumerated() { - let button = EPChipFilterButton() - button.text = tag - button.tag = index - button.addTarget(self, action: #selector(tagButtonTapped(_:)), for: .touchUpInside) - - stackView.addArrangedSubview(button) - tagButtons.append(button) - } - } - - @objc private func tagButtonTapped(_ sender: EPChipFilterButton) { - let index = sender.tag - - // 切换选中状态 - if selectedIndices.contains(index) { - selectedIndices.remove(index) - sender.isSelected = false - } else { - selectedIndices.insert(index) - sender.isSelected = true - } - - // 通知选择变更 - notifySelectionChanged() - } - - private func notifySelectionChanged() { - let selectedTags = getSelectedTags() - let selectedIndices = getSelectedIndices() - selectionCallback?(selectedTags, selectedIndices) - } -} diff --git a/crush/Crush/Src/Components/UI/CustomViews/IndicatorView.swift b/crush/Crush/Src/Components/UI/CustomViews/IndicatorView.swift deleted file mode 100644 index 2dcf37f..0000000 --- a/crush/Crush/Src/Components/UI/CustomViews/IndicatorView.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// IndicatorView.swift -// Crush -// -// Created by Leon on 2025/8/1. -// - -import UIKit - -class IndicatorView: UIView { - // MARK: - Properties - - private var icon: UIImageView! - private var circleColor: UIColor - private var _size: CGSize = CGSize(width: 16, height: 16) - - override var size: CGSize { - get { _size } - set { - _size = newValue - icon.snp.updateConstraints { make in - make.size.equalTo(newValue) - } - } - } - - var isAnimating: Bool { - return icon.layer.animation(forKey: "rotationAnimation") != nil - } - - // MARK: - Initialization - - init(color: UIColor) { - circleColor = color - super.init(frame: .zero) - initialViews() - } - - override init(frame: CGRect) { - circleColor = .black // Default color when not specified - super.init(frame: frame) - initialViews() - } - - required init?(coder: NSCoder) { - circleColor = .black - super.init(coder: coder) - initialViews() - } - - // MARK: - Setup - - private func initialViews() { - backgroundColor = .clear - - icon = UIImageView() - icon.contentMode = .scaleAspectFit - - let color = circleColor // Fallback to primary text color if needed - icon.image = generateIconImage(size: _size, color: color) - - addSubview(icon) - icon.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.size.equalTo(_size) - } - } - - // MARK: - Preset Style - - func setupGeneratingAIPicStyle(){ - size = CGSize(width: 96, height: 96) - icon.image = UIImage(named: "generating_indicator") - } - - // MARK: - Animation - - func startIndicator() { - isHidden = false - let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - rotationAnimation.toValue = NSNumber(value: Double.pi * 2.0) - rotationAnimation.duration = 1.0 - rotationAnimation.isRemovedOnCompletion = false - rotationAnimation.isCumulative = true - rotationAnimation.repeatCount = .infinity - icon.layer.add(rotationAnimation, forKey: "rotationAnimation") - } - - func stopIndicator() { - isHidden = true - icon.layer.removeAllAnimations() - } - - func startAnimating() { - startIndicator() - } - - func stopAnimating() { - stopIndicator() - } - - // MARK: - Helper Methods - - private func generateIconImage(icon: IconCode = .loading, size: CGSize, color: UIColor) -> UIImage? { - return MWIconFont.image(fromIcon: icon, size: size, color: color) - } - - // MARK: - Deinit - - deinit { - stopIndicator() - print("♻️ dealloc EPIndicatorView") - } -} diff --git a/crush/Crush/Src/Components/UI/CustomViews/LineView.swift b/crush/Crush/Src/Components/UI/CustomViews/LineView.swift deleted file mode 100644 index b047507..0000000 --- a/crush/Crush/Src/Components/UI/CustomViews/LineView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// LineView.swift -// Crush -// -// Created by Leon on 2025/8/4. -// - -import UIKit - -class LineView: UIView{ - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = .c.con - snp.makeConstraints { make in - make.height.equalTo(0.5) - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/crush/Crush/Src/Components/UI/CustomViews/PageIndicatorView.swift b/crush/Crush/Src/Components/UI/CustomViews/PageIndicatorView.swift deleted file mode 100644 index fb23601..0000000 --- a/crush/Crush/Src/Components/UI/CustomViews/PageIndicatorView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// PageIndicatorView.swift -// Crush -// -// Created by Leon on 2025/9/12. -// - -import UIKit - -class PageIndicatorView: UIView { - - private let topIndicator: UIView - private let bottomIndicator: UIView - - override init(frame: CGRect) { - topIndicator = UIView() - bottomIndicator = UIView() - - super.init(frame: frame) - - // 设置指示器容器的大小 - self.frame = CGRect(x: 0, y: 0, width: 4, height: 60) - self.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 4, height: 60)) - } - - cornerRadius = 2 - backgroundColor = CLGlobalToken.color(token: .glo_color_white)!.withAlphaComponent(0.3) - - // 配置顶部部分 - topIndicator.backgroundColor = CLSystemToken.color(token: .cswn)//.white - topIndicator.cornerRadius = 2 -// topIndicator.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - addSubview(topIndicator) - - // 配置底部部分 - bottomIndicator.backgroundColor = CLSystemToken.color(token: .cswn) - bottomIndicator.cornerRadius = 2 - bottomIndicator.isHidden = true - addSubview(bottomIndicator) - - // 初始设置布局为均分 - topIndicator.frame = CGRect(x: 0, y: 0, width: 4, height: 30) - bottomIndicator.frame = CGRect(x: 0, y: 30, width: 4, height: 30) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // 更新指示器的比例,传入一个进度值 (0.0 到 1.0) - func updateIndicator(progress: CGFloat) { - // 限制进度值范围 - let clampedProgress = max(0, min(progress, 1)) - - // 计算顶部和底部的高度 - let topHeight = clampedProgress * 60 - let bottomHeight = 60 - topHeight - - // 更新顶部和底部部分的高度 - topIndicator.frame = CGRect(x: 0, y: 0, width: 4, height: topHeight) - bottomIndicator.frame = CGRect(x: 0, y: topHeight, width: 4, height: bottomHeight) - - // 根据进度更新颜色 - if clampedProgress <= 0.5 { - topIndicator.isHidden = false - bottomIndicator.isHidden = true - // 页面在上半部分:上面白色,下面灰色 -// topIndicator.backgroundColor = .white -// bottomIndicator.backgroundColor = .gray - } else { - topIndicator.isHidden = true - bottomIndicator.isHidden = false - // 页面在下半部分:上面灰色,下面白色 -// topIndicator.backgroundColor = .gray -// bottomIndicator.backgroundColor = .white - } - } - - func updateIndicator(top:Bool){ - updateIndicator(progress: top ? 0.501 : 0.499) - } -} - diff --git a/crush/Crush/Src/Components/UI/CustomViews/TipWarningView.swift b/crush/Crush/Src/Components/UI/CustomViews/TipWarningView.swift deleted file mode 100644 index 424c765..0000000 --- a/crush/Crush/Src/Components/UI/CustomViews/TipWarningView.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// TipWarningView.swift -// Crush -// -// Created by Leon on 2025/8/4. -// - -import UIKit - -class TipWarningView: UIView { - var icon: UIImageView! - var contentLabel: LineSpaceLabel! - - var contentStr: String? { - didSet { - contentLabel.text = contentStr - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csnn - layer.cornerRadius = 16 - layer.masksToBounds = true - layer.borderWidth = 1 - layer.borderColor = UIColor.c.cevn.cgColor - - icon = { - let v = UIImageView() - v.image = UIImage(named: "warning_blue") - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(14) -// make.width.equalTo(16) -// make.height.equalTo(16) - } - return v - }() - - contentLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbm) - v.config(typo) - v.textColor = .c.ctpn - v.textAlignment = .left - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(icon.snp.trailing).offset(8) - make.trailing.equalToSuperview().offset(-16) - make.top.equalToSuperview().offset(12) - make.bottom.equalToSuperview().offset(-12) - } - return v - }() - } - - private func setupData() { - } - - private func setupEvent() { - } -} diff --git a/crush/Crush/Src/Components/UI/Dialog/BaseMaskPopDialogController.swift b/crush/Crush/Src/Components/UI/Dialog/BaseMaskPopDialogController.swift deleted file mode 100644 index 0c48c4c..0000000 --- a/crush/Crush/Src/Components/UI/Dialog/BaseMaskPopDialogController.swift +++ /dev/null @@ -1,256 +0,0 @@ -// -// BaseMaskPopDialogController.swift -// Crush -// -// Created by Leon on 2025/7/14. -// - -import UIKit - -enum EGMaskPopPriority: Int, Comparable { - case normal = 0 - case level1 = 1 - case level2 = 2 - case level3 = 3 - case level4 = 4 - case required = 10 - - // Implement Comparable protocol for priority comparison - static func < (lhs: EGMaskPopPriority, rhs: EGMaskPopPriority) -> Bool { - return lhs.rawValue < rhs.rawValue - } -} - -class BaseMaskPopDialogController: CLBaseViewController { - var bgMaskView = UIView() - var onlyshowInClass: AnyClass? - var block: UIView = UIView() - var closeButton: EPIconTertiaryButton! - - var priority: EGMaskPopPriority = .normal - var willDismissCalled: (() -> Void)? - var showActionCalled: (() -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .clear - - view.addSubview(bgMaskView) - bgMaskView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - bgMaskView.backgroundColor = .c.codr//.white.withAlphaComponent(0.65) - block = { - let v = UIView() - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - v.backgroundColor = .c.csbn - view.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.centerX.equalToSuperview() - make.width.equalTo(UIScreen.width * 0.8) - make.height.equalTo(360).priority(.low) - } - return v - }() - - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(tapCloseBtn), for: .touchUpInside) - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - guard let navc = navigationController else { - return - } - navc.setNavigationBarHidden(true, animated: animated) - } - - // MARK: - Class - - public static func getCurrentShowMaskPopVcIfHave() -> BaseMaskPopDialogController? { - guard let keyVc = UIWindow.getTopViewController() else { - return nil - } - - if let tabVc = keyVc.tabBarController { - for vc in tabVc.children { - if vc.isKind(of: BaseMaskPopDialogController.self) { - return vc as? BaseMaskPopDialogController - } - } - } - - if let navc = keyVc.navigationController { - for vc in navc.children { - if vc.isKind(of: BaseMaskPopDialogController.self) { - return vc as? BaseMaskPopDialogController - } - } - } - - if let presenting = keyVc.presentingViewController { - for vc in presenting.children { - if vc.isKind(of: BaseMaskPopDialogController.self) { - return vc as? BaseMaskPopDialogController - } - } - } - - return nil - } - - // MARK: - Show - - public func show() { - if let currentShowing = BaseMaskPopDialogController.getCurrentShowMaskPopVcIfHave() { - if currentShowing.priority < priority { // New dialog has higher priority - if priority == .required { - showForced() - return - } else { - DialogPopUpManager.shared.toBeHandleMaskPopControllers.append(currentShowing) - DialogPopUpManager.shared.toBeHandleMaskPopControllers.append(self) - showForced() - return - } - } else { - DialogPopUpManager.shared.toBeHandleMaskPopControllers.append(self) - print("♻️♻️♻️♻️♻️♻️♻️♻️ \(self) 弹窗被缓存 ♻️♻️♻️♻️♻️♻️♻️♻️") - return - } - } - - doLogicToFindShowVcShow() - } - - public func showForced() { - if EGNewBaseAlert.existingAlert() { - EGNewBaseAlert.hideAllAlert() - } - - if let popVc = BaseMaskPopDialogController.getCurrentShowMaskPopVcIfHave() { - if DialogPopUpManager.shared.toBeHandleMaskPopControllers.contains(popVc) { - popVc.dismiss(removePop: false) { [weak self] in - self?.doLogicToFindShowVcShow() - } - } else { - popVc.dismiss { [weak self] in - self?.doLogicToFindShowVcShow() - } - } - } else { - doLogicToFindShowVcShow() - } - } - - // MARK: - Helper - - private func commonContainerLRPadding() -> Double { - return UIScreen.main.bounds.size.width * (1 - 0.8) * 0.5 - } - - private func doLogicToFindShowVcShow() { - guard let keyVc = UIWindow.getTopViewController() else { - assert(false) - return - } - - if let onlyshowInClass = onlyshowInClass, !keyVc.isKind(of: onlyshowInClass) { - dismiss(completion: nil) - return - } - if let tabbarVc = keyVc.tabBarController { - subShow(keyVc, tabbar: tabbarVc) - } - -// let tabbarVc = keyVc?.tabBarController -// if let presentedVc = keyVc?.presentedViewController, presentedVc is EGUserCenterViewController { -// let vc = presentedVc as! EGUserCenterViewController -// vc.dismiss(animated: true) { [weak self] in -// self?.subShow(keyVc, tabbar: tabbarVc) -// } -// } else { -// subShow(keyVc, tabbar: tabbarVc) -// } - } - - private func subShow(_ keyVc: UIViewController?, tabbar tabbarVc: UITabBarController?) { - guard let keyVc = keyVc else { return } - - if let tabbarVc = tabbarVc { - tabbarVc.view.addSubview(view) - tabbarVc.addChild(self) - } else if let presentedVc = keyVc.presentedViewController { - presentedVc.view.addSubview(view) - presentedVc.addChild(self) - } else if let navController = keyVc.navigationController { - navController.view.addSubview(view) - navController.addChild(self) - navController.interactivePopGestureRecognizer?.isEnabled = false - } else { - keyVc.view.addSubview(view) - keyVc.addChild(self) - } - - view.alpha = 0 - UIView.animate(withDuration: 0.35, animations: { - self.view.alpha = 1 - }, completion: { _ in - self.viewDidAppear(true) - }) - view.layer.zPosition = 100 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - self.view.superview?.bringSubviewToFront(self.view) - } - - showActionCalled?() - } - - func dismiss(completion: (() -> Void)? = nil) { - dismiss(removePop: true, completion: completion) - } - - func dismiss(removePop: Bool, completion: (() -> Void)? = nil) { - willDismissCalled?() - - if let navigationController = navigationController { - navigationController.interactivePopGestureRecognizer?.isEnabled = true - } - removeFromParent() - - UIView.animate(withDuration: 0.35, animations: { - self.view.alpha = 0 - }, completion: { _ in - self.view.removeFromSuperview() - self.view.alpha = 1 - self.viewDidDisappear(true) - - if removePop, DialogPopUpManager.shared.toBeHandleMaskPopControllers.contains(self) { - DialogPopUpManager.shared.toBeHandleMaskPopControllers.removeAll { $0 === self } - } - - completion?() - DialogPopUpManager.shared.continueDialogIfHave() - }) - } - - public func resetPopController() { - view.removeFromSuperview() - removeFromParent() - } - - @objc private func tapCloseBtn(){ - dismiss() - } -} diff --git a/crush/Crush/Src/Components/UI/Dialog/DialogPopUpManager.swift b/crush/Crush/Src/Components/UI/Dialog/DialogPopUpManager.swift deleted file mode 100644 index e12a038..0000000 --- a/crush/Crush/Src/Components/UI/Dialog/DialogPopUpManager.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// DialogPopUpManager.swift -// Crush -// -// Created by Leon on 2025/7/14. -// - -import Foundation - -class DialogPopUpManager{ - public static let shared = DialogPopUpManager() - - lazy var toBeHandleMaskPopControllers: [BaseMaskPopDialogController] = [] - - public func disableSubsequentPopDialogs(){ - toBeHandleMaskPopControllers.removeAll() - } - - func continueDialogIfHave() { - if !toBeHandleMaskPopControllers.isEmpty { - var popVc = toBeHandleMaskPopControllers.first! - // Find the dialog with the highest priority, optionally restricted to a specific class - for vc in toBeHandleMaskPopControllers { - if vc.priority > popVc.priority { - if let onlyShowInClass = vc.onlyshowInClass, !UIWindow.getTopViewController()!.isKind(of: onlyShowInClass) { - // Skip if the current VC is not of the required class - } else { - popVc = vc - } - } - } - popVc.resetPopController() - popVc.show() - } - } - - func delayToContinueDialog() { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.continueDialogIfHave() - } - } -} diff --git a/crush/Crush/Src/Components/UI/Dialog/RoleIntroBrowseDialogController.swift b/crush/Crush/Src/Components/UI/Dialog/RoleIntroBrowseDialogController.swift deleted file mode 100644 index 9429632..0000000 --- a/crush/Crush/Src/Components/UI/Dialog/RoleIntroBrowseDialogController.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// RoleIntroBrowseDialogController.swift -// Crush -// -// Created by Leon on 2025/7/25. -// - -import UIKit - -/// 角色介绍 -class RoleIntroBrowseDialogController: BaseMaskPopDialogController { - var bgImageView: CLImageView! - var effectView: UIVisualEffectView! - var contentContainer: UIView! - - // var contentStackV: UIStackView! - var scrollContainer: LTScrollContainer! - - var titleLabel: UILabel! - var descLabel: LineSpaceLabel! - - /// Config - var introduction: String? - var imageUrl : String? - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - navigationView.isHidden = true - - bgMaskView.alpha = 0 - // block.backgroundColor = .black.withAlphaComponent(0.65) - block.backgroundColor = .clear - block.snp.remakeConstraints { make in - make.edges.equalToSuperview() - } - - bgImageView = { - let v = CLImageView() - view.insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - closeButton.update(radius: .round, iconSize: .large, iconCode: .delete) - closeButton.snp.remakeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom - 24) - } - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - v.alpha = 0.9 - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - contentContainer = { - let v = UIView() - v.backgroundColor = .clear - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight + 48) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom - 24 - 48 - 24 - 48) - } - return v - }() - -// contentStackV = { -// let v = UIStackView() -// v.axis = .vertical -// v.spacing = 24 -// v.alignment = .center -// contentContainer.addSubview(v) -// v.snp.makeConstraints { make in -// make.leading.equalToSuperview().offset(48) -// make.trailing.equalToSuperview().offset(-48) -// make.center.equalToSuperview() -// make.top.greaterThanOrEqualToSuperview() -// make.bottom.lessThanOrEqualToSuperview() -// } -// return v -// }() - - scrollContainer = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .center - v.stackEdge = UIEdgeInsets(top: 0, left: 48, bottom: 0, right: 48) - contentContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview()//.offset(48) - make.trailing.equalToSuperview()//.offset(-48) - make.top.bottom.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.ttm - scrollContainer.stack.addArrangedSubview(v) - return v - }() - - descLabel = { - let v = LineSpaceLabel() - v.textColor = .text - let typo = CLSystemToken.typography(token: .tbl) - v.config(typo) - scrollContainer.stack.addArrangedSubview(v) - return v - }() - - block.bringSubviewToFront(closeButton) - - titleLabel.text = "Intro" - } - - private func setupDats() { - descLabel.text = introduction - bgImageView.loadImage(imageUrl) -// #warning("test") -// testData() - } - - private func testData() { - descLabel.text = "She is a new intern teacher and has just graduated. You are the most rebellious student of the whole school workers. In order to prove her ability, she took the initiative to apply to be your class teacher. In the upcoming college entrance\nShe is a new intern teacher and has just graduated. You are the most rebellious student of the whole school workers. In order to prove her ability, she took the initiative to apply to be your class teacher. In the upcoming college entrance\nShe is a new intern teacher and has just graduated. You are the most rebellious student of the whole school workers. In order to prove her ability, she took the initiative to apply to be your class teacher. In the upcoming college entrance\nShe is a new intern teacher and has just graduated. You are the most rebellious student of the whole school workers. In order to prove her ability, she took the initiative to apply to be your class teacher. In the upcoming college entrance." -// descLabel.text = "She is a new intern teacher and has just graduated. You are the most rebellious student of the whole school workers. In order to prove her ability, she took the initiative to apply to be your class teacher. In the upcoming college entrance" - } - - private func setupEvents() { - } -} diff --git a/crush/Crush/Src/Components/UI/Dialog/UnlockPriceSetDialogController.swift b/crush/Crush/Src/Components/UI/Dialog/UnlockPriceSetDialogController.swift deleted file mode 100644 index a1f653b..0000000 --- a/crush/Crush/Src/Components/UI/Dialog/UnlockPriceSetDialogController.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// UnlockPriceSetDialogController.swift -// Crush -// -// Created by Leon on 2025/7/23. -// - -import UIKit - -/// ⚠️废弃 -class UnlockPriceSetDialogController: BaseMaskPopDialogController { - var titleLabel: UILabel! - var textFiled: CLTextField! - var button: StyleButton! - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - titleLabel = { - let v = UILabel() - v.textColor = .c.ctpn - v.font = .t.ttm - v.numberOfLines = 0 - block.addSubview(v) - v.textAlignment = .center - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(32) - } - return v - }() - - textFiled = { - let v = CLTextField() - block.addSubview(v) - v.keyboardType = .decimalPad - v.placeholder = "Price" - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24); - make.top.equalTo(titleLabel.snp.bottom).offset(24) - } - v.setupLeftCoinIconView() - - return v - }() - - button = { - let v = StyleButton() - v.primary(size: .large) - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24); - make.top.equalTo(textFiled.snp.bottom).offset(28) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel.text = "Unlock Method" - button.setTitle("Confirm", for: .normal) - } - - private func setupDats() { - } - - private func setupEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChanged(noti:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - } - - @objc func keyboardWillChanged(noti: Notification) { - guard let userInfo = noti.userInfo else { return } - let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval - let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue - //let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey]! as! Int - //let beginFrame = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue - - if endFrame.origin.y >= UIScreen.main.bounds.height { - UIView.animate(withDuration: duration) {[weak self] in - self?.block.snp.updateConstraints { make in - make.centerY.equalToSuperview() - } - self?.view.layoutIfNeeded() - } - }else{ - UIView.animate(withDuration: duration) {[weak self] in - self?.block.snp.updateConstraints { make in - make.centerY.equalToSuperview().offset(-endFrame.height * 0.5) - } - self?.view.layoutIfNeeded() - } - } - - - } -} diff --git a/crush/Crush/Src/Components/UI/Flag/EPIconFlagView.swift b/crush/Crush/Src/Components/UI/Flag/EPIconFlagView.swift deleted file mode 100644 index 4e0c7ad..0000000 --- a/crush/Crush/Src/Components/UI/Flag/EPIconFlagView.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// EPIconFlagView.swift -// Crush -// -// Created by Leon on 2025/7/25. -// - -import UIKit -import SnapKit - -class EPIconFlagView: UIImageView { - - // MARK: - Properties - - private let icon: UIImageView - - // MARK: - Initialization - - override init(frame: CGRect) { - icon = UIImageView() - super.init(frame: frame) - - // Setup constraints using SnapKit - snp.makeConstraints { make in - make.height.equalTo(24) - } - - addSubview(icon) - icon.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 12, height: 12)) - make.left.equalTo(6) - make.right.equalTo(-6) - make.centerY.equalToSuperview() - } - - contentMode = .scaleToFill - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Public Methods - - func setupRightTopLockStyle() { - configIconLocked(true) - layer.cornerRadius = 4 - layer.masksToBounds = true - // Right-top and left-bottom corners rounded - // layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMaxYCorner] - } - - func configIconLocked(_ lock: Bool) { - if lock { - icon.image = MWIconFont.image(fromIcon: .iconPrivate, size: .init(width: 16, height: 16), color: .text) - backgroundColor = .c.csedn - image = nil - } else { - - icon.image = MWIconFont.image(fromIcon: .iconPublic, size: .init(width: 16, height: 16), color: .text) - let gradient = CLSystemToken.gradient(token: .cpgn) - image = gradient.toImage(size: CGSize(width: 40, height: 32)) - } - } -} diff --git a/crush/Crush/Src/Components/UI/Hud/AnimatingView.swift b/crush/Crush/Src/Components/UI/Hud/AnimatingView.swift deleted file mode 100644 index a8614c1..0000000 --- a/crush/Crush/Src/Components/UI/Hud/AnimatingView.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// AnimatingView.swift -// Crush -// -// Created by Leon on 2025/8/23. -// - -import UIKit -import APNGKit - -enum AnimatingViewType { -case generating -} - -/// APNG动画,loadingView,以APNG格式的图片为主 -class AnimatingView: UIView{ - var iv : APNGImageView! - - var type: AnimatingViewType = .generating - - private override init(frame: CGRect) { - super.init(frame: frame) - } - - convenience init(type: AnimatingViewType = .generating) { - self.init(frame: .zero) - self.type = type - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .clear - iv = { - let v = APNGImageView() - v.contentMode = .scaleAspectFit - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - switch type { - case .generating: - if let url = Bundle.main.url(forResource: "generating", withExtension: "png") { - let image = try? APNGImage(fileURL: url) - iv.image = image - } - } - } - -} diff --git a/crush/Crush/Src/Components/UI/Hud/Hud.swift b/crush/Crush/Src/Components/UI/Hud/Hud.swift deleted file mode 100644 index 7ed1351..0000000 --- a/crush/Crush/Src/Components/UI/Hud/Hud.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// Hud.swift -// Crush -// -// Created by Leon on 2025/7/13. -// - -import Foundation -import UIKit - -/// ⚠️只在不确认View主体时使用(eg: IAPCore),其他情况请使用Hud.showIndicator() and view.hideToastActivity -class Hud { - static let shared = Hud() - - static func showIndicator(_ blockUserTap: Bool? = true) { - DispatchQueue.main.async { - if let view = UIWindow.key { - view.makeToastActivity(.center, blockUserInteraction: blockUserTap ?? true) - } else if let view = UIWindow.getTopViewController()?.view { - view.makeToastActivity(.center, blockUserInteraction: blockUserTap ?? true) - } - } - } - - /// 显示加载指示器,可选择是否阻止用户交互 - /// - Parameter blockUserInteraction: 是否阻止用户与页面上其他元素的交互,默认为false -// static func showIndicator(blockUserInteraction: Bool = true) { -// DispatchQueue.main.async { -// if let view = UIWindow.key { -// view.makeToastActivity(.center, blockUserInteraction: blockUserInteraction) -// } else if let view = UIWindow.getTopViewController()?.view { -// view.makeToastActivity(.center, blockUserInteraction: blockUserInteraction) -// } -// } -// } - - static func hideIndicator() { - DispatchQueue.main.async { - if let view = UIWindow.key { - view.hideToastActivity() - } else if let view = UIWindow.getTopViewController()?.view { - view.hideToastActivity() - } - } - } - - static func toast(str: String?, completion: ((Bool)-> Void)? = nil) { - DispatchQueue.main.async { - guard let toast = str, let view = UIWindow.applicationKey else { - return - } - view.makeToast(toast, completion: completion) - } - } - - static func hideToast(){ - DispatchQueue.main.async { - guard let view = UIWindow.applicationKey else { - return - } - view.hideToast() - } - } - - static func comingsoon(){ - Hud.toast(str: "Comming soon...") - } - - required init() { - //ToastManager.shared.duration = 1.0 - - ToastManager.shared.isQueueEnabled - } -} diff --git a/crush/Crush/Src/Components/UI/Hud/Toast.swift b/crush/Crush/Src/Components/UI/Hud/Toast.swift deleted file mode 100644 index cc308f0..0000000 --- a/crush/Crush/Src/Components/UI/Hud/Toast.swift +++ /dev/null @@ -1,831 +0,0 @@ -// -// Toast.swift -// Toast-Swift -// -// Copyright (c) 2015-2024 Charles Scalesse. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import ObjectiveC -import UIKit -import Lottie - -/** - Toast is a Swift extension that adds toast notifications to the `UIView` object class. - It is intended to be simple, lightweight, and easy to use. Most toast notifications - can be triggered with a single line of code. - - The `makeToast` methods create a new view and then display it as toast. - - The `showToast` methods display any view as toast. - - */ -public extension UIView { - /** - Keys used for associated objects. - */ - private struct ToastKeys { - static var timer = malloc(1) - static var duration = malloc(1) - static var point = malloc(1) - static var completion = malloc(1) - static var activeToasts = malloc(1) - static var activityView = malloc(1) - static var queue = malloc(1) - static var maskView = malloc(1) - } - - /** - Swift closures can't be directly associated with objects via the - Objective-C runtime, so the (ugly) solution is to wrap them in a - class that can be used with associated objects. - */ - private class ToastCompletionWrapper { - let completion: ((Bool) -> Void)? - - init(_ completion: ((Bool) -> Void)?) { - self.completion = completion - } - } - - private enum ToastError: Error { - case missingParameters - } - - private var activeToasts: NSMutableArray { - if let activeToasts = objc_getAssociatedObject(self, &ToastKeys.activeToasts) as? NSMutableArray { - return activeToasts - } else { - let activeToasts = NSMutableArray() - objc_setAssociatedObject(self, &ToastKeys.activeToasts, activeToasts, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return activeToasts - } - } - - private var queue: NSMutableArray { - if let queue = objc_getAssociatedObject(self, &ToastKeys.queue) as? NSMutableArray { - return queue - } else { - let queue = NSMutableArray() - objc_setAssociatedObject(self, &ToastKeys.queue, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return queue - } - } - - // MARK: - Make Toast Methods - - /** - Creates and presents a new toast view. - - @param message The message to be displayed - @param duration The toast duration - @param position The toast's position - @param title The title - @param image The image - @param style The style. The shared style will be used when nil - @param completion The completion closure, executed after the toast view disappears. - didTap will be `true` if the toast view was dismissed from a tap. - */ - func makeToast(_ message: String?, duration: TimeInterval = ToastManager.shared.duration, position: ToastPosition = ToastManager.shared.position, title: String? = nil, image: UIImage? = nil, style: ToastStyle = ToastManager.shared.style, completion: ((_ didTap: Bool) -> Void)? = nil) { - do { - let toast = try toastViewForMessage(message, title: title, image: image, style: style) - showToast(toast, duration: duration, position: position, completion: completion) - } catch ToastError.missingParameters { - print("Error: message, title, and image are all nil") - } catch {} - } - - /** - Creates a new toast view and presents it at a given center point. - - @param message The message to be displayed - @param duration The toast duration - @param point The toast's center point - @param title The title - @param image The image - @param style The style. The shared style will be used when nil - @param completion The completion closure, executed after the toast view disappears. - didTap will be `true` if the toast view was dismissed from a tap. - */ - func makeToast(_ message: String?, duration: TimeInterval = ToastManager.shared.duration, point: CGPoint, title: String?, image: UIImage?, style: ToastStyle = ToastManager.shared.style, completion: ((_ didTap: Bool) -> Void)?) { - do { - let toast = try toastViewForMessage(message, title: title, image: image, style: style) - showToast(toast, duration: duration, point: point, completion: completion) - } catch ToastError.missingParameters { - print("Error: message, title, and image cannot all be nil") - } catch {} - } - - // MARK: - Show Toast Methods - - /** - Displays any view as toast at a provided position and duration. The completion closure - executes when the toast view completes. `didTap` will be `true` if the toast view was - dismissed from a tap. - - @param toast The view to be displayed as toast - @param duration The notification duration - @param position The toast's position - @param completion The completion block, executed after the toast view disappears. - didTap will be `true` if the toast view was dismissed from a tap. - */ - func showToast(_ toast: UIView, duration: TimeInterval = ToastManager.shared.duration, position: ToastPosition = ToastManager.shared.position, completion: ((_ didTap: Bool) -> Void)? = nil) { - let point = position.centerPoint(forToast: toast, inSuperview: self) - showToast(toast, duration: duration, point: point, completion: completion) - } - - /** - Displays any view as toast at a provided center point and duration. The completion closure - executes when the toast view completes. `didTap` will be `true` if the toast view was - dismissed from a tap. - - @param toast The view to be displayed as toast - @param duration The notification duration - @param point The toast's center point - @param completion The completion block, executed after the toast view disappears. - didTap will be `true` if the toast view was dismissed from a tap. - */ - func showToast(_ toast: UIView, duration: TimeInterval = ToastManager.shared.duration, point: CGPoint, completion: ((_ didTap: Bool) -> Void)? = nil) { - objc_setAssociatedObject(toast, &ToastKeys.completion, ToastCompletionWrapper(completion), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - if ToastManager.shared.isQueueEnabled, activeToasts.count > 0 { - objc_setAssociatedObject(toast, &ToastKeys.duration, NSNumber(value: duration), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - objc_setAssociatedObject(toast, &ToastKeys.point, NSValue(cgPoint: point), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - queue.add(toast) - } else { - showToast(toast, duration: duration, point: point) - } - } - - // MARK: - Hide Toast Methods - - /** - Hides the active toast. If there are multiple toasts active in a view, this method - hides the oldest toast (the first of the toasts to have been presented). - - @see `hideAllToasts()` to remove all active toasts from a view. - - @warning This method has no effect on activity toasts. Use `hideToastActivity` to - hide activity toasts. - - */ - func hideToast() { - guard let activeToast = activeToasts.firstObject as? UIView else { return } - hideToast(activeToast) - } - - /** - Hides an active toast. - - @param toast The active toast view to dismiss. Any toast that is currently being displayed - on the screen is considered active. - - @warning this does not clear a toast view that is currently waiting in the queue. - */ - func hideToast(_ toast: UIView) { - guard activeToasts.contains(toast) else { return } - hideToast(toast, fromTap: false) - } - - /** - Hides all toast views. - - @param includeActivity If `true`, toast activity will also be hidden. Default is `false`. - @param clearQueue If `true`, removes all toast views from the queue. Default is `true`. - */ - func hideAllToasts(includeActivity: Bool = false, clearQueue: Bool = true) { - if clearQueue { - clearToastQueue() - } - - activeToasts.compactMap { $0 as? UIView } - .forEach { hideToast($0) } - - if includeActivity { - hideToastActivity() - } - } - - /** - Removes all toast views from the queue. This has no effect on toast views that are - active. Use `hideAllToasts(clearQueue:)` to hide the active toasts views and clear - the queue. - */ - func clearToastQueue() { - queue.removeAllObjects() - } - - // MARK: - Activity Methods - - /** - Creates and displays a new toast activity indicator view at a specified position. - - @warning Only one toast activity indicator view can be presented per superview. Subsequent - calls to `makeToastActivity(position:)` will be ignored until `hideToastActivity()` is called. - - @warning `makeToastActivity(position:)` works independently of the `showToast` methods. Toast - activity views can be presented and dismissed while toast views are being displayed. - `makeToastActivity(position:)` has no effect on the queueing behavior of the `showToast` methods. - - @param position The toast's position - @param blockUserInteraction Whether to block user interaction with other elements. Default is false. - */ - func makeToastActivity(_ position: ToastPosition = .center, blockUserInteraction: Bool = false) { - // sanity - guard objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView == nil else { return } - - let toast = createToastActivityView() - let point = position.centerPoint(forToast: toast, inSuperview: self) - makeToastActivity(toast, point: point, blockUserInteraction: blockUserInteraction) - } - - /** - Creates and displays a new toast activity indicator view at a specified position. - - @warning Only one toast activity indicator view can be presented per superview. Subsequent - calls to `makeToastActivity(position:)` will be ignored until `hideToastActivity()` is called. - - @warning `makeToastActivity(position:)` works independently of the `showToast` methods. Toast - activity views can be presented and dismissed while toast views are being displayed. - `makeToastActivity(position:)` has no effect on the queueing behavior of the `showToast` methods. - - @param point The toast's center point - @param blockUserInteraction Whether to block user interaction with other elements. Default is false. - */ - func makeToastActivity(_ point: CGPoint, blockUserInteraction: Bool = false) { - // sanity - guard objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView == nil else { return } - - let toast = createToastActivityView() - makeToastActivity(toast, point: point, blockUserInteraction: blockUserInteraction) - } - - /** - Dismisses the active toast activity indicator view. - */ - func hideToastActivity() { - if let toast = objc_getAssociatedObject(self, &ToastKeys.activityView) as? UIView { - UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: { - toast.alpha = 0.0 - }) { _ in - toast.removeFromSuperview() - objc_setAssociatedObject(self, &ToastKeys.activityView, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - // 移除遮罩层 - if let maskView = objc_getAssociatedObject(self, &ToastKeys.maskView) as? UIView { - UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: { - maskView.alpha = 0.0 - }) { _ in - maskView.removeFromSuperview() - objc_setAssociatedObject(self, &ToastKeys.maskView, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - } - - // MARK: - Helper Methods - - /** - Returns `true` if a toast view or toast activity view is actively being displayed. - */ - func isShowingToast() -> Bool { - return activeToasts.count > 0 || objc_getAssociatedObject(self, &ToastKeys.activityView) != nil - } - - // MARK: - Private Activity Methods - - private func makeToastActivity(_ toast: UIView, point: CGPoint, blockUserInteraction: Bool = false) { - toast.alpha = 0.0 - toast.center = point - - objc_setAssociatedObject(self, &ToastKeys.activityView, toast, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - // 如果需要阻止用户交互,添加遮罩层 - if blockUserInteraction { - let maskView = createMaskView() - objc_setAssociatedObject(self, &ToastKeys.maskView, maskView, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - addSubview(maskView) - maskView.addSubview(toast) - } else { - addSubview(toast) - } - - UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: .curveEaseOut, animations: { - toast.alpha = 1.0 - }) - } - - - private func createToastActivityView() -> UIView { - let style = ToastManager.shared.style - - let activityView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: style.activitySize.width, height: style.activitySize.height)) - activityView.backgroundColor = style.activityBackgroundColor - activityView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin] - activityView.layer.cornerRadius = style.cornerRadius - - if style.displayShadow { - activityView.layer.shadowColor = style.shadowColor.cgColor - activityView.layer.shadowOpacity = style.shadowOpacity - activityView.layer.shadowRadius = style.shadowRadius - activityView.layer.shadowOffset = style.shadowOffset - } - - // let activityIndicatorView = UIActivityIndicatorView(style: .large) - // activityIndicatorView.center = CGPoint(x: activityView.bounds.size.width / 2.0, y: activityView.bounds.size.height / 2.0) - // activityView.addSubview(activityIndicatorView) - // activityIndicatorView.color = style.activityIndicatorColor - // activityIndicatorView.startAnimating() - - let animation = LottieAnimation.named("single_ring") - let animationView = LottieAnimationView(animation: animation) - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - let size = CGSize(width: 40, height: 40) - animationView.size = size - animationView.backgroundColor = .clear - activityView.addSubview(animationView) - animationView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(size) - } - animationView.play() - - return activityView - } - - private func createMaskView() -> UIView { - let maskView = UIView(frame: bounds) - maskView.backgroundColor = UIColor.clear - maskView.isUserInteractionEnabled = true - maskView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - return maskView - } - - // MARK: - Private Show/Hide Methods - - private func showToast(_ toast: UIView, duration: TimeInterval, point: CGPoint) { - toast.center = point - toast.alpha = 0.0 - - if ToastManager.shared.isTapToDismissEnabled { - let recognizer = UITapGestureRecognizer(target: self, action: #selector(UIView.handleToastTapped(_:))) - toast.addGestureRecognizer(recognizer) - toast.isUserInteractionEnabled = true - toast.isExclusiveTouch = true - } - - activeToasts.add(toast) - addSubview(toast) - - let timer = Timer(timeInterval: duration, target: self, selector: #selector(UIView.toastTimerDidFinish(_:)), userInfo: toast, repeats: false) - objc_setAssociatedObject(toast, &ToastKeys.timer, timer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseOut, .allowUserInteraction], animations: { - toast.alpha = 1.0 - }) { _ in - guard let timer = objc_getAssociatedObject(toast, &ToastKeys.timer) as? Timer else { return } - RunLoop.main.add(timer, forMode: .common) - } - - UIAccessibility.post(notification: .screenChanged, argument: toast) - } - - private func hideToast(_ toast: UIView, fromTap: Bool) { - if let timer = objc_getAssociatedObject(toast, &ToastKeys.timer) as? Timer { - timer.invalidate() - } - - UIView.animate(withDuration: ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.curveEaseIn, .beginFromCurrentState], animations: { - toast.alpha = 0.0 - }) { _ in - toast.removeFromSuperview() - self.activeToasts.remove(toast) - - if let wrapper = objc_getAssociatedObject(toast, &ToastKeys.completion) as? ToastCompletionWrapper, let completion = wrapper.completion { - completion(fromTap) - } - - if let nextToast = self.queue.firstObject as? UIView, let duration = objc_getAssociatedObject(nextToast, &ToastKeys.duration) as? NSNumber, let point = objc_getAssociatedObject(nextToast, &ToastKeys.point) as? NSValue { - self.queue.removeObject(at: 0) - self.showToast(nextToast, duration: duration.doubleValue, point: point.cgPointValue) - } - } - } - - // MARK: - Events - - @objc - private func handleToastTapped(_ recognizer: UITapGestureRecognizer) { - guard let toast = recognizer.view else { return } - hideToast(toast, fromTap: true) - } - - @objc - private func toastTimerDidFinish(_ timer: Timer) { - guard let toast = timer.userInfo as? UIView else { return } - hideToast(toast) - } - - // MARK: - Toast Construction - - /** - Creates a new toast view with any combination of message, title, and image. - The look and feel is configured via the style. Unlike the `makeToast` methods, - this method does not present the toast view automatically. One of the `showToast` - methods must be used to present the resulting view. - - @warning if message, title, and image are all nil, this method will throw - `ToastError.missingParameters` - - @param message The message to be displayed - @param title The title - @param image The image - @param style The style. The shared style will be used when nil - @throws `ToastError.missingParameters` when message, title, and image are all nil - @return The newly created toast view - */ - func toastViewForMessage(_ message: String?, title: String?, image: UIImage?, style: ToastStyle) throws -> UIView { - // sanity - guard message != nil || title != nil || image != nil else { - throw ToastError.missingParameters - } - - var messageLabel: UILabel? - var titleLabel: UILabel? - var imageView: UIImageView? - - let wrapperView = UIView() - wrapperView.backgroundColor = style.backgroundColor - wrapperView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin] - wrapperView.layer.cornerRadius = style.cornerRadius - - if style.displayShadow { - wrapperView.layer.shadowColor = style.shadowColor.cgColor - wrapperView.layer.shadowOpacity = style.shadowOpacity - wrapperView.layer.shadowRadius = style.shadowRadius - wrapperView.layer.shadowOffset = style.shadowOffset - } - - if let image = image { - imageView = UIImageView(image: image) - imageView?.contentMode = .scaleAspectFit - imageView?.frame = CGRect(x: style.horizontalPadding, y: style.verticalPadding, width: style.imageSize.width, height: style.imageSize.height) - } - - var imageRect = CGRect.zero - - if let imageView = imageView { - imageRect.origin.x = style.horizontalPadding - imageRect.origin.y = style.verticalPadding - imageRect.size.width = imageView.bounds.size.width - imageRect.size.height = imageView.bounds.size.height - } - - if let title = title { - titleLabel = UILabel() - titleLabel?.numberOfLines = style.titleNumberOfLines - titleLabel?.font = style.titleFont - titleLabel?.textAlignment = style.titleAlignment - titleLabel?.lineBreakMode = .byTruncatingTail - titleLabel?.textColor = style.titleColor - titleLabel?.backgroundColor = UIColor.clear - titleLabel?.text = title - - let maxTitleSize = CGSize(width: (bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: bounds.size.height * style.maxHeightPercentage) - let titleSize = titleLabel?.sizeThatFits(maxTitleSize) - if let titleSize = titleSize { - titleLabel?.frame = CGRect(x: 0.0, y: 0.0, width: titleSize.width, height: titleSize.height) - } - } - - if let message = message { - messageLabel = UILabel() - messageLabel?.text = message - messageLabel?.numberOfLines = style.messageNumberOfLines - messageLabel?.font = style.messageFont - messageLabel?.textAlignment = style.messageAlignment - messageLabel?.lineBreakMode = .byTruncatingTail - messageLabel?.textColor = style.messageColor - messageLabel?.backgroundColor = UIColor.clear - - let maxMessageSize = CGSize(width: (bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: bounds.size.height * style.maxHeightPercentage) - let messageSize = messageLabel?.sizeThatFits(maxMessageSize) - if let messageSize = messageSize { - let actualWidth = min(messageSize.width, maxMessageSize.width) - let actualHeight = min(messageSize.height, maxMessageSize.height) - messageLabel?.frame = CGRect(x: 0.0, y: 0.0, width: actualWidth, height: actualHeight) - } - } - - var titleRect = CGRect.zero - - if let titleLabel = titleLabel { - titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding - titleRect.origin.y = style.verticalPadding - titleRect.size.width = titleLabel.bounds.size.width - titleRect.size.height = titleLabel.bounds.size.height - } - - var messageRect = CGRect.zero - - if let messageLabel = messageLabel { - messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding - messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding - messageRect.size.width = messageLabel.bounds.size.width - messageRect.size.height = messageLabel.bounds.size.height - } - - let longerWidth = max(titleRect.size.width, messageRect.size.width) - let longerX = max(titleRect.origin.x, messageRect.origin.x) - let wrapperWidth = max(imageRect.size.width + (style.horizontalPadding * 2.0), longerX + longerWidth + style.horizontalPadding) - - let textMaxY = messageRect.size.height <= 0.0 && titleRect.size.height > 0.0 ? titleRect.maxY : messageRect.maxY - let wrapperHeight = max(textMaxY + style.verticalPadding, imageRect.size.height + (style.verticalPadding * 2.0)) - - wrapperView.frame = CGRect(x: 0.0, y: 0.0, width: wrapperWidth, height: wrapperHeight) - - if let titleLabel = titleLabel { - titleRect.size.width = longerWidth - titleLabel.frame = titleRect - wrapperView.addSubview(titleLabel) - } - - if let messageLabel = messageLabel { - messageRect.size.width = longerWidth - messageLabel.frame = messageRect - wrapperView.addSubview(messageLabel) - } - - if let imageView = imageView { - wrapperView.addSubview(imageView) - } - - return wrapperView - } -} - -// MARK: - Toast Style - -/** - `ToastStyle` instances define the look and feel for toast views created via the - `makeToast` methods as well for toast views created directly with - `toastViewForMessage(message:title:image:style:)`. - - @warning `ToastStyle` offers relatively simple styling options for the default - toast view. If you require a toast view with more complex UI, it probably makes more - sense to create your own custom UIView subclass and present it with the `showToast` - methods. - */ -public struct ToastStyle { - public init() {} - - /** - The background color. Default is `.black` at 80% opacity. - */ - public var backgroundColor: UIColor = UIColor.black.withAlphaComponent(0.8) - - /** - The title color. Default is `UIColor.whiteColor()`. - */ - public var titleColor: UIColor = .white - - /** - The message color. Default is `.white`. - */ - public var messageColor: UIColor = .white - - /** - A percentage value from 0.0 to 1.0, representing the maximum width of the toast - view relative to it's superview. Default is 0.8 (80% of the superview's width). - */ - public var maxWidthPercentage: CGFloat = 0.8 { - didSet { - maxWidthPercentage = max(min(maxWidthPercentage, 1.0), 0.0) - } - } - - /** - A percentage value from 0.0 to 1.0, representing the maximum height of the toast - view relative to it's superview. Default is 0.8 (80% of the superview's height). - */ - public var maxHeightPercentage: CGFloat = 0.8 { - didSet { - maxHeightPercentage = max(min(maxHeightPercentage, 1.0), 0.0) - } - } - - /** - The spacing from the horizontal edge of the toast view to the content. When an image - is present, this is also used as the padding between the image and the text. - Default is 10.0. - - */ - public var horizontalPadding: CGFloat = 10.0 - - /** - The spacing from the vertical edge of the toast view to the content. When a title - is present, this is also used as the padding between the title and the message. - Default is 10.0. On iOS11+, this value is added added to the `safeAreaInset.top` - and `safeAreaInsets.bottom`. - */ - public var verticalPadding: CGFloat = 10.0 - - /** - The corner radius. Default is 10.0. - */ - public var cornerRadius: CGFloat = 10.0 - - /** - The title font. Default is `.boldSystemFont(16.0)`. - */ - public var titleFont: UIFont = .boldSystemFont(ofSize: 16.0) - - /** - The message font. Default is `.systemFont(ofSize: 16.0)`. - */ - public var messageFont: UIFont = .systemFont(ofSize: 16.0) - - /** - The title text alignment. Default is `NSTextAlignment.Left`. - */ - public var titleAlignment: NSTextAlignment = .left - - /** - The message text alignment. Default is `NSTextAlignment.Left`. - */ - public var messageAlignment: NSTextAlignment = .left - - /** - The maximum number of lines for the title. The default is 0 (no limit). - */ - public var titleNumberOfLines = 0 - - /** - The maximum number of lines for the message. The default is 0 (no limit). - */ - public var messageNumberOfLines = 0 - - /** - Enable or disable a shadow on the toast view. Default is `false`. - */ - public var displayShadow = false - - /** - The shadow color. Default is `.black`. - */ - public var shadowColor: UIColor = .black - - /** - A value from 0.0 to 1.0, representing the opacity of the shadow. - Default is 0.8 (80% opacity). - */ - public var shadowOpacity: Float = 0.8 { - didSet { - shadowOpacity = max(min(shadowOpacity, 1.0), 0.0) - } - } - - /** - The shadow radius. Default is 6.0. - */ - public var shadowRadius: CGFloat = 6.0 - - /** - The shadow offset. The default is 4 x 4. - */ - public var shadowOffset = CGSize(width: 4.0, height: 4.0) - - /** - The image size. The default is 80 x 80. - */ - public var imageSize = CGSize(width: 80.0, height: 80.0) - - /** - The size of the toast activity view when `makeToastActivity(position:)` is called. - Default is 100 x 100. - */ - public var activitySize = CGSize(width: 100.0, height: 100.0) - - /** - The fade in/out animation duration. Default is 0.2. - */ - public var fadeDuration: TimeInterval = 0.2 - - /** - Activity indicator color. Default is `.white`. - */ - public var activityIndicatorColor: UIColor = .white - - /** - Activity background color. Default is `.black` at 80% opacity. - */ - public var activityBackgroundColor: UIColor = UIColor.black.withAlphaComponent(0.8) -} - -// MARK: - Toast Manager - -/** - `ToastManager` provides general configuration options for all toast - notifications. Backed by a singleton instance. - */ -public class ToastManager { - /** - The `ToastManager` singleton instance. - - */ - public static let shared = ToastManager() - - /** - The shared style. Used whenever toastViewForMessage(message:title:image:style:) is called - with with a nil style. - - */ - public var style = ToastStyle() - - /** - Enables or disables tap to dismiss on toast views. Default is `true`. - - */ - public var isTapToDismissEnabled = true - - /** - Enables or disables queueing behavior for toast views. When `true`, - toast views will appear one after the other. When `false`, multiple toast - views will appear at the same time (potentially overlapping depending - on their positions). This has no effect on the toast activity view, - which operates independently of normal toast views. Default is `false`. - - */ - public var isQueueEnabled = false - - /** - The default duration. Used for the `makeToast` and - `showToast` methods that don't require an explicit duration. - Default is 3.0. - - */ - public var duration: TimeInterval = 1.0 - - /** - Sets the default position. Used for the `makeToast` and - `showToast` methods that don't require an explicit position. - Default is `ToastPosition.Bottom`. - - */ - public var position: ToastPosition = .center -} - -// MARK: - ToastPosition - -public enum ToastPosition { - case top - case center - case bottom - - fileprivate func centerPoint(forToast toast: UIView, inSuperview superview: UIView) -> CGPoint { - let topPadding: CGFloat = ToastManager.shared.style.verticalPadding + superview.csSafeAreaInsets.top - let bottomPadding: CGFloat = ToastManager.shared.style.verticalPadding + superview.csSafeAreaInsets.bottom - - switch self { - case .top: - return CGPoint(x: superview.bounds.size.width / 2.0, y: (toast.frame.size.height / 2.0) + topPadding) - case .center: - return CGPoint(x: superview.bounds.size.width / 2.0, y: superview.bounds.size.height / 2.0) - case .bottom: - return CGPoint(x: superview.bounds.size.width / 2.0, y: (superview.bounds.size.height - (toast.frame.size.height / 2.0)) - bottomPadding) - } - } -} - -// MARK: - Private UIView Extensions - -private extension UIView { - var csSafeAreaInsets: UIEdgeInsets { - if #available(iOS 11.0, *) { - return self.safeAreaInsets - } else { - return .zero - } - } -} diff --git a/crush/Crush/Src/Components/UI/Label/CLIconLabel.swift b/crush/Crush/Src/Components/UI/Label/CLIconLabel.swift deleted file mode 100644 index 75ca1c2..0000000 --- a/crush/Crush/Src/Components/UI/Label/CLIconLabel.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// CLIconLabel.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -/// icon: 12x12 textLabel: txt.label.s -class CLIconLabel: UIView { - // MARK: - Properties - - /// Default: 12x12 icon image view - let iconImageView: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - /// Label with default text style - let contentLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .c.ctpn - label.font = .t.tls // 12 - label.textAlignment = .left - return label - }() - - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = 4 - stackView.distribution = .equalSpacing - stackView.axis = .horizontal - stackView.alignment = .center - stackView.setContentHuggingPriority(UILayoutPriority(rawValue: 256), for: .horizontal) - return stackView - }() - - /// Default: 12x12 - var iconSize: CGSize?{ - didSet{ - iconImageView.snp.remakeConstraints { make in - make.size.equalTo(iconSize ?? CGSize(width: 12, height: 12)) - } - } - } - - /// Configurable left and right padding for the stack view - var innerLRPadding: CGFloat = 0 { - didSet { - // Update stack view constraints when padding changes - stackView.snp.remakeConstraints { make in - make.centerY.equalTo(self) - make.leading.equalTo(self).offset(self.innerLRPadding) - make.trailing.equalTo(self).offset(-self.innerLRPadding) - make.top.greaterThanOrEqualToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - } - } - - var spacing:CGFloat = 4{ - didSet{ - stackView.spacing = spacing - } - } - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupViews() - } - - // MARK: - Setup - - private func setupViews() { - stackView.setContentHuggingPriority(UILayoutPriority(258), for: .horizontal) - iconImageView.setContentHuggingPriority(UILayoutPriority(rawValue: 254), for: .horizontal) - iconImageView.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 754), for: .horizontal) - contentLabel.setContentHuggingPriority(UILayoutPriority(rawValue: 254), for: .horizontal) - contentLabel.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 754), for: .horizontal) - - addSubview(stackView) - - // Configure stack view constraints - stackView.snp.makeConstraints { make in - make.leading.equalTo(self).offset(innerLRPadding) - make.trailing.equalTo(self).offset(-innerLRPadding) - make.centerY.equalTo(self) - make.top.greaterThanOrEqualToSuperview() - make.bottom.lessThanOrEqualToSuperview() - } - - // Add subviews to stack view - stackView.addArrangedSubview(iconImageView) - stackView.addArrangedSubview(contentLabel) - - // Set icon image view size (12x12 as per original) - iconImageView.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 12, height: 12)) - } - } - - // MARK: - Helper - - private func commonCoinAndLabel(){ - - } -} diff --git a/crush/Crush/Src/Components/UI/Label/CLLabel.swift b/crush/Crush/Src/Components/UI/Label/CLLabel.swift deleted file mode 100644 index d99a84d..0000000 --- a/crush/Crush/Src/Components/UI/Label/CLLabel.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CLLabel.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit - -class CLLabel: UILabel{ - convenience init() { - self.init(frame: .zero) - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - textColor = .white - - setContentCompressionResistancePriority(UILayoutPriority(742), for: .horizontal) - } - -} diff --git a/crush/Crush/Src/Components/UI/Label/ColorLabel.swift b/crush/Crush/Src/Components/UI/Label/ColorLabel.swift deleted file mode 100644 index a599b8e..0000000 --- a/crush/Crush/Src/Components/UI/Label/ColorLabel.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// ColorLabel.swift -// Crush -// -// Created by Leon on 2025/7/27. -// - -import UIKit - -class ColorLabel: UILabel { - - enum GradientStyle { - case sunset // 两色渐变 - case ocean // 三色渐变 - case theme // 两色渐变 - case vip // VIP - case custom(colors: [UIColor]) // 可自定义颜色数组 - - var colors: [UIColor] { - switch self { - case .sunset: - return [UIColor.red, UIColor.orange] - case .ocean: - return [UIColor.cyan, UIColor.blue, UIColor.purple] - case .theme: - let gradient = CLSystemToken.gradient(token: .cpgn) - return gradient.colors() - case .vip: - let gradient = CLSystemToken.gradient(token: .ccvn) - return gradient.colors() - case .custom(let colors): - return colors - } - } - } - - private var gradientStyle: GradientStyle? - private var gradientLayer: CAGradientLayer? - - /// 设置渐变样式 - func applyGradient(_ style: GradientStyle) { - self.gradientStyle = style - setNeedsLayout() - } - - func clearGradient(){ - self.gradientStyle = nil - gradientLayer?.removeFromSuperlayer() - setNeedsLayout() - } - - override func layoutSubviews() { - super.layoutSubviews() - - guard let style = gradientStyle, bounds.size.width > 0 , bounds.size.height > 0 else { return } - - gradientLayer?.removeFromSuperlayer() - - let gradient = CAGradientLayer() - gradient.frame = bounds - gradient.colors = style.colors.map { $0.cgColor } - gradient.startPoint = CGPoint(x: 0, y: 0.5) - gradient.endPoint = CGPoint(x: 1, y: 0.5) - - // 用渐变图层做文字填充 - gradient.mask = textLayer() - - layer.addSublayer(gradient) - gradientLayer = gradient - } - - private func textLayer() -> CALayer { - let text = self.text ?? "" - let font = self.font ?? UIFont.systemFont(ofSize: 17) - - let label = UILabel() - label.frame = bounds - label.text = text - label.font = font - label.textAlignment = textAlignment - label.textColor = .black // 不影响遮罩,只为了内容 - - UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0) - label.layer.render(in: UIGraphicsGetCurrentContext()!) - let maskImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - let maskLayer = CALayer() - maskLayer.contents = maskImage?.cgImage - maskLayer.frame = bounds - return maskLayer - } -} diff --git a/crush/Crush/Src/Components/UI/Label/EPOptionFlagLabel.swift b/crush/Crush/Src/Components/UI/Label/EPOptionFlagLabel.swift deleted file mode 100644 index 8de822a..0000000 --- a/crush/Crush/Src/Components/UI/Label/EPOptionFlagLabel.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// EPOptionFlagLabel.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit - -class EPOptionFlagLabel: UILabel { - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - - font = CLSystemToken.font(token: .tls) // EPSystemToken.typography(.txtLabelS).font - text = "(\(NSLocalizedString("optional", comment: "")))" - setContentCompressionResistancePriority(.init(756), for: .horizontal) - textColor = .c.ctsn - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/crush/Crush/Src/Components/UI/Label/EPTagLabel.swift b/crush/Crush/Src/Components/UI/Label/EPTagLabel.swift deleted file mode 100644 index 2c5ba9b..0000000 --- a/crush/Crush/Src/Components/UI/Label/EPTagLabel.swift +++ /dev/null @@ -1,277 +0,0 @@ -// -// EPTagLabel.swift -// Crush -// -// Created by Leon on 2025/7/25. -// - -import SnapKit // Assuming SnapKit is used for layout instead of Masonry -import UIKit - -// MARK: - Enums - -enum EPTagLabelStyle: Int { - // 灰色底,白色字 - case `default` - case whiteOnColor - // 黑底,白色字 - case blackOnColor - // 紫色底,白色字 - case primary - case warning - case positive - case emphasis - case discount - case subscribe - // 金色边框渐变 - case legends - case important - // 金黄色渐变 - case vipOfficial - // 红色渐变 - case limited - // 浅紫到深紫 - case aiTag -} - -enum EPTagLabelSize: Int { - case size20 // height: 20 - case small // Mobile Txt/txt.label.s height: 24 - case medium // Mobile Txt/txt.label.s height: 28 - case large // Mobile Txt/txt.label.m height: 32 -} - -// Assuming NSStringShowFromCommunityIdentifier is a localization function -func NSStringShowFromCommunityIdentifier(_ identifier: String) -> String { - // Placeholder implementation - return identifier.capitalized -} - -class EPTagLabel: UIView { - // MARK: - Properties - - private let contentStackH: UIStackView - private(set) var textLabel: UILabel - private var cgColors: [CGColor] = [] - private var gradientLayer: CAGradientLayer? - private var startPoint: CGPoint = CGPoint(x: 0, y: 0) - private var endPoint: CGPoint = CGPoint(x: 1, y: 0) - - var style: EPTagLabelStyle { - didSet { setupTheme() } - } - - var labelSize: EPTagLabelSize { - didSet { - setupLayout() - setupFont() - } - } - - var text: String? { - didSet { - textLabel.text = text - setNeedsLayout() - } - } - - // MARK: - Initialization - - init(style: EPTagLabelStyle, size: EPTagLabelSize = .small, text: String? = nil) { - self.style = style - labelSize = size - textLabel = UILabel() - contentStackH = UIStackView() - - textLabel.text = text - - super.init(frame: .zero) - - addSubview(contentStackH) - contentStackH.addArrangedSubview(textLabel) - - self.text = text - startPoint = CGPoint(x: 0, y: 0) - endPoint = CGPoint(x: 1, y: 0) - - clipsToBounds = true - layer.cornerRadius = CLSystemToken.radius(token: .rxs) - - setupTheme() - setupLayout() - setupFont() - - // Setup stack view - setupContentStack() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Public Methods - - static func innerPadding() -> CGFloat { - return 8 - } - - func setupContentStackHUnlimitedHeight() { - var textLRPadding = Self.innerPadding() - if labelSize == .size20 { - textLRPadding = 4 - } - - let edge = UIEdgeInsets(top: 7.5, left: textLRPadding, bottom: 7.5, right: textLRPadding) - contentStackH.snp.remakeConstraints { make in - make.edges.equalToSuperview().inset(edge) - } - } - - // MARK: - Layout - - override func layoutSubviews() { - super.layoutSubviews() - - gradientLayer?.removeFromSuperlayer() - gradientLayer = nil - - if !cgColors.isEmpty { - let gradientLayer = CAGradientLayer() - gradientLayer.frame = bounds - gradientLayer.colors = cgColors - gradientLayer.startPoint = startPoint - gradientLayer.endPoint = endPoint - gradientLayer.locations = cgColors.count >= 3 ? [0, 0.5, 1] : [0, 1] - layer.insertSublayer(gradientLayer, at: 0) - self.gradientLayer = gradientLayer - } - } - - // MARK: - Private Setup Methods - - private func setupContentStack() { - contentStackH.spacing = 8 - contentStackH.alignment = .center - addSubview(contentStackH) - - textLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - contentStackH.addArrangedSubview(textLabel) - } - - private func setupLayout() { - var labelH: CGFloat = 24 - var textLRPadding = Self.innerPadding() - - switch labelSize { - case .size20: - labelH = 20 - textLRPadding = 4 - case .medium: - labelH = 28 - case .large: - labelH = 32 - case .small: - break - } - - let edge = UIEdgeInsets(top: 0, left: textLRPadding, bottom: 0, right: textLRPadding) - contentStackH.snp.remakeConstraints { make in - make.edges.equalToSuperview().inset(edge) - make.height.equalTo(labelH) - } - } - - private func setupFont() { - switch labelSize { - case .small, .medium: - textLabel.font = .t.tls // EPSystemToken.typography("EPS_Txt_Label_s") - case .large: - textLabel.font = .t.tlm // EPSystemToken.typography("EPS_Txt_Label_m") - case .size20: - textLabel.font = .t.tls // EPSystemToken.typography("EPS_Txt_Label_s") - } - } - - private func setupTheme() { - // Reset state - textLabel.textColor = .text // EPSystemToken.color("EPS_Color_Txt_primary_normal") - - cgColors = [] - backgroundColor = nil - layer.borderWidth = 0 - - switch style { - case .default: - backgroundColor = .c.csen // EPSystemToken.color("EPS_Color_Surface_element_normal") - - case .whiteOnColor: - backgroundColor = .c.cseln // EPSystemToken.color("EPS_Color_Surface_element_light_normal") - - case .blackOnColor: - backgroundColor = .c.csedn // EPSystemToken.color("EPS_Color_Surface_element_dark_normal") - - case .primary: - backgroundColor = .c.cpn // EPSystemToken.color("EPS_Color_Primary_normal") - - case .warning: - backgroundColor = .c.cwn // EPSystemToken.color("EPS_Color_Warning_normal") - - case .positive: - backgroundColor = .c.cpn // EPSystemToken.color("EPS_Color_Positive_normal") - - case .emphasis: - backgroundColor = .c.cen // EPSystemToken.color("EPS_Color_Emphasis_normal") - - case .important: - backgroundColor = .c.cin // EPSystemToken.color("EPS_Color_Important_normal") - - case .discount: - let token = CLSystemToken.gradient(token: .cpgn) // EPSystemToken.gradient("EPS_Color_Primary_Gradient_normal") - setupColorsGradient(token) - - case .subscribe: - let token = CLSystemToken.gradient(token: .ccvn) // EPSystemToken.gradient("EPS_Color_Context_subscribe_normal") - setupColorsGradient(token) - - case .legends: - backgroundColor = .c.csedn // EPSystemToken.color("EPS_Color_Surface_element_dark_normal") - layer.borderWidth = 1 - layer.borderColor = UIColor.c.csedn.cgColor // EPSystemToken.color("EPS_Color_Context_legends_variant_normal").cgColor - textLabel.textColor = .c.csedn // EPSystemToken.color("EPS_Color_Context_legends_variant_normal") - - case .vipOfficial: - textLabel.textColor = .c.cbd // EPSystemToken.color("EPS_Color_Background_default") - let token = CLSystemToken.gradient(token: .ccvn) // EPSystemToken.gradient("EPS_Color_Context_legends_normal") - setupColorsGradient(token) - - case .limited: - let token = CLSystemToken.gradient(token: .cign) // EPSystemToken.gradient("EPS_Color_Important_Gradient_normal") - setupColorsGradient(token) - - case .aiTag: - setupColors([ - UIColor(hex: "#D327FF"), - UIColor(hex: "#773DFF"), - ]) - } - - setNeedsLayout() - } - - private func setupColorsGradient(_ token: EPGradient) { - cgColors = [] - if let firstColor = token.firstColor { - cgColors.append(firstColor.cgColor) - } - if let secondColor = token.secondColor { - cgColors.append(secondColor.cgColor) - } - setNeedsLayout() - } - - private func setupColors(_ colors: [UIColor]) { - cgColors = colors.map { $0.cgColor } - setNeedsLayout() - } -} diff --git a/crush/Crush/Src/Components/UI/Label/LineSpaceLabel.swift b/crush/Crush/Src/Components/UI/Label/LineSpaceLabel.swift deleted file mode 100644 index 34ca0b8..0000000 --- a/crush/Crush/Src/Components/UI/Label/LineSpaceLabel.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// LineSpaceLabel.swift -// Crush -// -// Created by Leon on 2025/7/18. -// -import UIKit - -class LineSpaceLabel: UILabel { - // MARK: - Properties - - var paragraphSpace: CGFloat = 0 - - private var typography: EPTypography? - private var typographyText: String? - - // MARK: - Initialization - - override init(frame: CGRect) { - super.init(frame: frame) - numberOfLines = 0 - textColor = .text - guard let text = typographyText, !text.isEmpty else { return } - self.text = text - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Public Methods (Way 1) - - func setupLineSpace(_ space: CGFloat) { - guard let text = text, !text.isEmpty else { - assertionFailure("self.text should not be empty.") - return - } - - let attributedString: NSMutableAttributedString - if let existingAttributedText = attributedText { - attributedString = NSMutableAttributedString(attributedString: existingAttributedText) - } else { - attributedString = NSMutableAttributedString(string: text) - } - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = space - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: text.count)) - - self.text = nil - attributedText = attributedString - lineBreakMode = .byTruncatingTail - } - - func setupText(_ text: String?, lineSpace: CGFloat) { - let safeText = text ?? "" - - let attributedString = NSMutableAttributedString(string: safeText) - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = lineSpace - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: safeText.count)) - - attributedText = attributedString - lineBreakMode = .byTruncatingTail - } - - func setupHighlightText(_ highlight: String?, color: UIColor) { - guard let attributedText = attributedText, let highlight = highlight, !highlight.isEmpty else { - assertionFailure("attributedText and highlight must be valid") - return - } - - let attributedString = NSMutableAttributedString(attributedString: attributedText) - if let range = attributedString.string.range(of: highlight) { - let nsRange = NSRange(range, in: attributedString.string) - attributedString.addAttribute(.foregroundColor, value: color, range: nsRange) - } - self.attributedText = attributedString - } - - // MARK: - Public Methods (Way 2) - - func config(_ typography: EPTypography) { - self.typography = typography - self.text = text - } - - // MARK: - Overrides - - override var text: String? { - get { super.text } - set { - let safeText = newValue ?? "" - - if let typography = typography { - typographyText = safeText - let attributes: [NSAttributedString.Key: Any] = [ - .font: typography.font!, - .foregroundColor: textColor ?? .black, - ] - let attributedString = NSMutableAttributedString(string: safeText, attributes: attributes) - - let calLineHeight = typography.lineHeight * 0.5 // Temporary conversion, may need testing - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = calLineHeight - paragraphStyle.alignment = textAlignment - if paragraphSpace > 0 { - paragraphStyle.paragraphSpacing = paragraphSpace - } - attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: safeText.count)) - - self.attributedText = attributedString - self.lineBreakMode = .byTruncatingTail - - layoutIfNeeded() - } else { - super.text = safeText - } - } - } - - override var textColor: UIColor! { - get { super.textColor } - set { - super.textColor = newValue - if let typographyText = typographyText, !typographyText.isEmpty { - text = typographyText - } - } - } -} diff --git a/crush/Crush/Src/Components/UI/Label/PaddingLabel.swift b/crush/Crush/Src/Components/UI/Label/PaddingLabel.swift deleted file mode 100755 index ed4e772..0000000 --- a/crush/Crush/Src/Components/UI/Label/PaddingLabel.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// UILabel.swift -// DouYinSwift5 -// -// Created by lym on 2020/7/23. -// Copyright © 2020 lym. All rights reserved. -// - -import UIKit - -class PaddingLabel: UILabel { - // 1.定义一个接受间距的属性 - var padding = UIEdgeInsets.zero - - // 2. 返回 label 重新计算过 text 的 rectangle - override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect { - guard text != nil else { - return super.textRect(forBounds: bounds, limitedToNumberOfLines: numberOfLines) - } - - let insetRect = bounds.inset(by: padding) - let textRect = super.textRect(forBounds: insetRect, limitedToNumberOfLines: numberOfLines) - let invertedInsets = UIEdgeInsets(top: -padding.top, - left: -padding.left, - bottom: -padding.bottom, - right: -padding.right) - return textRect.inset(by: invertedInsets) - } - - // 3. 绘制文本时,对当前 rectangle 添加间距 - override func drawText(in rect: CGRect) { - super.drawText(in: rect.inset(by: padding)) - } -} diff --git a/crush/Crush/Src/Components/UI/NaviBar/NavigationView.swift b/crush/Crush/Src/Components/UI/NaviBar/NavigationView.swift deleted file mode 100644 index 6ecb7da..0000000 --- a/crush/Crush/Src/Components/UI/NaviBar/NavigationView.swift +++ /dev/null @@ -1,192 +0,0 @@ -// -// NavigationView.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import Foundation -import UIKit -//import R - -@objcMembers class NavigationView: UIView { - - var alpha0Title:String?{ - didSet{ - titleLabel.alpha = 0 - titleLabel.text = alpha0Title - } - } - - var title:String?{ - didSet{ - titleLabel.text = title - } - } - - /// \> 0 - var paddingRightForRightStack:CGFloat = 2{ - didSet{ - rightStackH.snp.updateConstraints { make in - make.trailing.equalToSuperview().offset(-paddingRightForRightStack) - } - } - } - - var tapBackButtonAction : (() -> Void)? - var bgView:UIView! - - @objc lazy var leftStackH : UIStackView = { - let v = UIStackView() - v.spacing = 0 - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(8) // 8+insets.left(16) = 24 - make.bottom.equalToSuperview() - make.height.equalTo(44) - } - return v - }() - - @objc lazy var backButton: UIButton = { - let backButton = UIButton(type: .custom) - leftStackH.addArrangedSubview(backButton) - backButton.setImage(R.image.nav_back_white()!, for: .normal) - backButton.contentHorizontalAlignment = .left - backButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: -16) - backButton.addTarget(self, action: #selector(popAction), for: .touchUpInside) - backButton.snp.makeConstraints { make in - make.width.equalTo(44) - make.height.equalTo(44) - } - return backButton - }() - - @objc lazy var titleLabel: UILabel = { - let titleLabel = UILabel() - titleLabel.textColor = .c.ctpn - titleLabel.preferredMaxLayoutWidth = 120 - titleLabel.textAlignment = .center - titleLabel.font = .t.ttm//.fredokaOne(size: 18) - addSubview(titleLabel) - - titleLabel.snp.makeConstraints { make in - make.centerY.equalTo(backButton) - make.centerX.equalToSuperview() - make.leading.greaterThanOrEqualTo(leftStackH.snp.trailing) - make.trailing.lessThanOrEqualTo(rightStackH.snp.leading) - } - return titleLabel - }() - - /// btn 44x44 is ok - @objc lazy var rightStackH: UIStackView = { - let rightStackH = UIStackView() - rightStackH.axis = .horizontal - rightStackH.spacing = 0 - rightStackH.alignment = .center - addSubview(rightStackH) - rightStackH.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-paddingRightForRightStack) - make.centerY.equalTo(backButton) - } - return rightStackH - }() - - @objc lazy var rightButton: UIButton = { - let rightButton = UIButton(type: .custom) - rightButton.isHidden = true - rightButton.setTitleColor(.black, for: .normal) - rightButton.titleLabel?.font = .t.tbm//.popSemiBold(size: 16) - rightButton.contentHorizontalAlignment = .right - rightStackH.addArrangedSubview(rightButton) - rightButton.snp.makeConstraints { make in - make.height.equalTo(44) - make.width.equalTo(44) - } - return rightButton - }() - - @objc lazy var styleMainButton: StyleButton = { - let styleMainButton = StyleButton() - styleMainButton.primary(size: .small) - rightStackH.addArrangedSubview(styleMainButton) - paddingRightForRightStack = 24 - return styleMainButton - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - bgView = { - let v = UIView() - addSubview(v) - v.backgroundColor = .c.cbd - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - //backgroundColor = .c.cbd - backgroundColor = .clear - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc func popAction() { - if let block = tapBackButtonAction{ - block() - }else{ - if let vc = viewController() as? CLBaseViewController { - vc.close() - }else{ - viewController()?.navigationController?.popViewController(animated: true) - } - } - } - - @objc open func setupBackButtonCloseIcon(black: Bool = false) { - if black { - backButton.setImage(R.image.icon_close_20_black(), for: .normal) - } else { - backButton.setImage(R.image.icon_close_20(), for: .normal) - - } - backButton.isHidden = false - } -} - -// MARK: - Public -// 支持渐变 NavigationView -extension NavigationView { - - //⚠️ Please use NaviAlphaHandle to adjust alpha - func changeAlpha(alpha: CGFloat) { - titleLabel.isHidden = false - - if titleLabel.alpha == alpha { - return - } - - backgroundColor = UIColor.white.withAlphaComponent(alpha) - titleLabel.alpha = alpha - backButton.alpha = alpha - } - - func setupNaviRightStyleButton(){ - styleMainButton.isHidden = false - paddingRightForRightStack = 24 - } - - func setupBgViewToStatusBarHeight(){ - self.bgView.snp.remakeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(UIWindow.statusBarHeight) - } - } -} diff --git a/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleCell.swift b/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleCell.swift deleted file mode 100644 index 5d99f45..0000000 --- a/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleCell.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// JXSegmentedTagStyleCell.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -import JXSegmentedView - -class JXSegmentedTagStyleCell: JXSegmentedTitleCell{ - let bgColorView = UIView() - let bgCornerView = UIView() - - open override func commonInit() { - super.commonInit() - - contentView.insertSubview(bgColorView, at: 0) - bgColorView.snp.makeConstraints { make in - //make.edges.equalToSuperview() - make.leading.trailing.equalToSuperview() - make.centerY.equalToSuperview() - make.height.equalTo(32) - } - - contentView.insertSubview(bgCornerView, aboveSubview: bgColorView) - bgCornerView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.centerY.equalToSuperview() - make.height.equalTo(32) - } - } - - open override func layoutSubviews() { - super.layoutSubviews() - -// guard let myItemModel = itemModel as? JXSegmentedTagStyleItemModel else { -// return -// } - - // Use autolayout - } - - - open override func reloadData(itemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType) { - super.reloadData(itemModel: itemModel, selectedType: selectedType ) - - guard let myItemModel = itemModel as? JXSegmentedTagStyleItemModel else { - return - } - - bgColorView.backgroundColor = itemModel.isSelected ? myItemModel.selectBackgroundColor : myItemModel.normalBackgroundColor - bgColorView.cornerRadius = myItemModel.backgroundCornerRadius - - bgCornerView.cornerRadius = myItemModel.backgroundCornerRadius - bgCornerView.layer.borderWidth = myItemModel.borderLineWidth - bgCornerView.layer.borderColor = itemModel.isSelected ? myItemModel.selectBorderColor.cgColor : myItemModel.normalBorderColor.cgColor - bgCornerView.backgroundColor = .clear - - if myItemModel.backgroundHeight != 32{ - bgColorView.snp.updateConstraints { make in - make.height.equalTo(myItemModel.backgroundHeight) - } - bgCornerView.snp.updateConstraints { make in - make.height.equalTo(myItemModel.backgroundHeight) - } - } - - } -} diff --git a/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleDataSource.swift b/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleDataSource.swift deleted file mode 100644 index 1c18a13..0000000 --- a/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleDataSource.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// JXSegmentedTagStyle.swift -// Crush -// -// Created by Leon on 2025/9/9. -// -import JXSegmentedView -class JXSegmentedTagStyleDataSource : JXSegmentedTitleDataSource { - var normalBackgroundColor: UIColor = .c.csen - var normalBorderColor: UIColor = .c.csen - var selectBackgroundColor: UIColor = .c.cpn - var selectBorderColor:UIColor = .c.con - - var borderLineWidth:CGFloat = 1 - var backgroundCornerRadius :CGFloat = 16 // 32*0.5 - var backgroundWidth : CGFloat = 60 - var backgroundHeight : CGFloat = 32 - - override init() { - super.init() - - isItemSpacingAverageEnabled = false - itemSpacing = 12 - titleNormalColor = .c.ctpn - titleSelectedColor = .c.ctpn - titleNormalFont = .t.tlm - titleSelectedFont = .t.tlm - - itemWidthIncrement = 32 - } - - open override func preferredItemModelInstance() -> JXSegmentedBaseItemModel { - return JXSegmentedTagStyleItemModel() - } - - open override func preferredRefreshItemModel(_ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int) { - super.preferredRefreshItemModel(itemModel, at: index, selectedIndex: selectedIndex) - - guard let itemModel = itemModel as? JXSegmentedTagStyleItemModel else { - return - } - - itemModel.normalBackgroundColor = normalBackgroundColor - itemModel.normalBorderColor = normalBorderColor - itemModel.selectBackgroundColor = selectBackgroundColor - itemModel.selectBorderColor = selectBorderColor - itemModel.borderLineWidth = borderLineWidth - - itemModel.backgroundWidth = backgroundWidth - itemModel.backgroundHeight = backgroundHeight - itemModel.backgroundCornerRadius = backgroundHeight * 0.5//backgroundCornerRadius - - - // ... - } - - //MARK: - JXSegmentedViewDataSource - open override func registerCellClass(in segmentedView: JXSegmentedView) { - segmentedView.collectionView.register(JXSegmentedTagStyleCell.self, forCellWithReuseIdentifier: "cell") - } - - open override func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell { - let cell = segmentedView.dequeueReusableCell(withReuseIdentifier: "cell", at: index) - return cell - } -} - -extension JXSegmentedTagStyleDataSource{ - func standardTagStyle(){ - - } -} diff --git a/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleItemModel.swift b/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleItemModel.swift deleted file mode 100644 index b4b80f3..0000000 --- a/crush/Crush/Src/Components/UI/PagingView/BgSegmentedView/JXSegmentedTagStyleItemModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// JXSegmentedTagStyleItemModel.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -import UIKit -import JXSegmentedView - -class JXSegmentedTagStyleItemModel: JXSegmentedTitleItemModel{ - var normalBackgroundColor: UIColor = .c.csen - var normalBorderColor: UIColor = .clear//.c.csen - var selectBackgroundColor: UIColor = .c.cpn - var selectBorderColor:UIColor = .c.con - - var borderLineWidth:CGFloat = 1 - var backgroundCornerRadius :CGFloat = 16 // 32*0.5 - var backgroundWidth : CGFloat = 60 - var backgroundHeight : CGFloat = 32 -} diff --git a/crush/Crush/Src/Components/UI/PagingView/CLSegmentedViews.swift b/crush/Crush/Src/Components/UI/PagingView/CLSegmentedViews.swift deleted file mode 100644 index 6914cf8..0000000 --- a/crush/Crush/Src/Components/UI/PagingView/CLSegmentedViews.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CLSegmentedView.swift -// Crush -// -// Created by Leon on 2025/7/15. -// - -import Foundation -import JXSegmentedView - -// Style - -extension JXSegmentedTitleDataSource{ - func clStyle(){ - titleNormalFont = .systemFont(ofSize: 14) - titleSelectedFont = .boldSystemFont(ofSize: 14) - titleNormalColor = .gray - titleSelectedColor = .white - } -} - -extension JXSegmentedIndicatorLineView{ - func clStyle(){ - indicatorWidth = JXSegmentedViewAutomaticDimension - indicatorWidth = 20 - indicatorHeight = 2 - indicatorColor = .orange - verticalOffset = 8 - } -} diff --git a/crush/Crush/Src/Components/UI/PagingView/PagingView.swift b/crush/Crush/Src/Components/UI/PagingView/PagingView.swift deleted file mode 100644 index c5f8265..0000000 --- a/crush/Crush/Src/Components/UI/PagingView/PagingView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// PagingView.swift -// Crush -// -// Created by Leon on 2025/7/20. -// -import JXPagingView -import JXSegmentedView - -extension JXPagingListContainerView: @retroactive JXSegmentedViewListContainer {} - -extension JXSegmentedTitleDataSource { - public func clNormalStyle() { - isItemSpacingAverageEnabled = false - // itemWidth = UIScreen.width / 2 - itemSpacing = 32 - titleNormalColor = .c.ctsn - titleSelectedColor = .c.ctpn - titleNormalFont = .t.tll - titleSelectedFont = .t.tll - } - - public func naviTitleStyle(){ - isItemSpacingAverageEnabled = false - itemSpacing = 32 - titleNormalColor = .c.ctsn - titleSelectedColor = .c.ctpn - titleNormalFont = .t.ths - titleSelectedFont = .t.ths - } - - public func searchListMainStyle(){ - isItemSpacingAverageEnabled = true - titleNormalColor = .c.ctsn - titleSelectedColor = .c.ctpn - titleNormalFont = .t.tll - titleSelectedFont = .t.tll - } -} - -extension JXSegmentedView{ - /// 主要设置indicator - public func clNormalStyle() { - contentEdgeInsetLeft = 24 - contentEdgeInsetRight = 24 - - let indicator = JXSegmentedIndicatorLineView() - indicator.indicatorWidth = JXSegmentedViewAutomaticDimension - indicator.indicatorWidth = 16 - indicator.indicatorHeight = 2 - indicator.indicatorColor = .c.cpn - indicator.verticalOffset = 4 - indicators = [indicator] - } - - public func naviTitleStyle(){ - contentEdgeInsetLeft = 24 - contentEdgeInsetRight = 24 - - let indicator = JXSegmentedIndicatorLineView() - indicator.indicatorWidth = JXSegmentedViewAutomaticDimension - indicator.indicatorWidth = 20 - indicator.indicatorHeight = 3 - indicator.indicatorColor = .c.cpn - indicator.verticalOffset = 4 - indicators = [indicator] - } - - public func searchListMainStyle(){ - let indicator = JXSegmentedIndicatorLineView() - indicator.indicatorWidth = JXSegmentedViewAutomaticDimension - indicator.indicatorWidth = 20 - indicator.indicatorHeight = 3 - indicator.indicatorColor = .c.cpn - indicator.verticalOffset = 4 - indicators = [indicator] - } -} diff --git a/crush/Crush/Src/Components/UI/Picker/SelectCommonController.swift b/crush/Crush/Src/Components/UI/Picker/SelectCommonController.swift deleted file mode 100644 index da1d639..0000000 --- a/crush/Crush/Src/Components/UI/Picker/SelectCommonController.swift +++ /dev/null @@ -1,345 +0,0 @@ -// -// SelectCommonController.swift -// Crush -// -// Created by Leon on 2025/7/25. -// - -import SnapKit -import UIKit - -protocol SelectCommonModelProtocol: AnyObject { - func displayStr() -> String - - func sourceObj() -> Any -} - -class SelectCommonController: CLBaseViewController { - // MARK: - Properties from header - - var vcTitle: String? - var isMultiChoose: Bool = false - var atlastSelectOne: Bool = false - var cellClass: UITableViewCell.Type? - var datas: [SelectCommonModelProtocol] = [] - var markSelected: [SelectCommonModelProtocol] = [] - var tapSingleAction: ((SelectCommonModelProtocol) -> Void)? - var tapMultiConfrimAction: (([SelectCommonModelProtocol]) -> Void)? - var specialCellForSignInFirstOption: SelectCommonModelProtocol? - - // MARK: - Private Properties - - private lazy var titleView: TitleView = { - let view = TitleView() - return view - }() - - private lazy var tableView: UITableView = { - let tableView = UITableView() - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - tableView.delegate = self - tableView.dataSource = self - tableView.contentInsetAdjustmentBehavior = .never - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIWindow.safeAreaBottom, right: 0) - - if let cellClass = cellClass { - tableView.register(cellClass, forCellReuseIdentifier: String(describing: cellClass)) - } else { - tableView.register(SelectCommonTableCell.self, forCellReuseIdentifier: String(describing: SelectCommonTableCell.self)) - } - - return tableView - }() - - private var tempSelectModels: [SelectCommonModelProtocol] = [] - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupData() - setupEvents() - } - - // MARK: - Setup - - private func setupUI() { - title = "Select" - - navigationView.setupBackButtonCloseIcon() - - view.addSubview(tableView) - tableView.snp.makeConstraints { make in - make.top.equalTo(navigationView.snp.bottom) - make.left.right.bottom.equalToSuperview() - } - - titleView.title = vcTitle - titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: titleView.preCalculateHeight()) - tableView.tableHeaderView = titleView - - if isMultiChoose { - navigationView.styleMainButton.isHidden = false - navigationView.styleMainButton.setTitle("Confirm", for: .normal) - refreshMultiChooseConfirmBtn() - } - - view.setNeedsDisplay() - view.layoutIfNeeded() - } - - private func tapNaviConfirm() { - tapMultiConfrimAction?(tempSelectModels) - markSelected = tempSelectModels - dismiss(animated: true, completion: nil) - } - - private func setupData() { - if markSelected.count == 1, let firstSelected = markSelected.first, - let index = datas.firstIndex(where: { $0 === firstSelected }) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: false) - } - - tempSelectModels = markSelected.isEmpty ? [] : markSelected - } - - private func setupEvents() { - } - - // MARK: - Class Methods - - static func navController() -> CLNavigationController { - let vc = SelectCommonController() - return CLNavigationController(rootViewController: vc) - } - - // MARK: - Helpers - - private func refreshMultiChooseConfirmBtn() { - guard isMultiChoose else { - assertionFailure("Should not call refreshMultiChooseConfirmBtn when not in multi-choose mode") - return - } - navigationView.styleMainButton.isEnabled = atlastSelectOne ? !tempSelectModels.isEmpty : true - } -} - -// MARK: - UITableViewDelegate, UITableViewDataSource - -extension SelectCommonController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return datas.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let data = datas[indexPath.row] - - if let cellClass = cellClass as? SelectCommonBaseCell.Type { - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: cellClass), for: indexPath) as! SelectCommonBaseCell - cell.config(data: data) - cell.configSelect(selected: markSelected.contains { $0 === data }) - return cell - } else { - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SelectCommonTableCell.self), for: indexPath) as! SelectCommonTableCell - cell.isMultiChoose = isMultiChoose - cell.config(data: data) - cell.configSelect(selected: markSelected.contains { $0 === data }) - - if let specialOption = specialCellForSignInFirstOption, specialOption === data { - cell.configSignInFirstMode(true) - } else { - cell.configSignInFirstMode(false) - } - return cell - } - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let data = datas[indexPath.row] - - if specialCellForSignInFirstOption != nil && specialCellForSignInFirstOption === data { - return - } - - if isMultiChoose { - if let index = tempSelectModels.firstIndex(where: { $0 === data }) { - tempSelectModels.remove(at: index) - } else { - tempSelectModels.append(data) - } - markSelected = tempSelectModels - tableView.reloadData() - refreshMultiChooseConfirmBtn() - } else { - markSelected = [data] - tapSingleAction?(data) - dismiss(animated: true, completion: nil) - } - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !(vcTitle?.isEmpty ?? true) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } - } -} - -// MARK: - SelectCommonModel - -class SelectCommonModel: SelectCommonModelProtocol { - var name: String? - var objInfo: Any? - var integerInfo: Int = 0 - - func displayStr() -> String { - return name ?? "" - } - - func sourceObj() -> Any { - return objInfo as Any - } -} - -class SelectCommonBaseCell: UITableViewCell { - 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 - } - - func config(data: SelectCommonModelProtocol) { - // Base implementation, to be overridden by subclasses - } - - func configSelect(selected: Bool) { - // Base implementation, to be overridden by subclasses - } -} - -class SelectCommonTableCell: SelectCommonBaseCell { - // MARK: - Properties - - private lazy var label: UILabel = { - let label = UILabel() - label.font = .t.tbl // EPSystemToken.typography(.bodyL).font - label.numberOfLines = 0 - return label - }() - - private lazy var checkButton: EPRadioButton = { - let button = EPRadioButton() - button.isUserInteractionEnabled = false - button.isSelected = true - return button - }() - - private lazy var line: UIView = { - let view = UIView() - return view - }() - - private lazy var signInFirstTipLabel: UILabel = { - let label = UILabel() - label.textColor = .c.cttn - label.font = .t.tbs - label.text = "Please Sign In" - label.isHidden = true - return label - }() - - private var signInFirstMode: Bool = false - - var isMultiChoose: Bool = false { - didSet { - if isMultiChoose { - checkButton.setupStyle(.rectangle) - } - } - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.backgroundColor = .clear - backgroundColor = .clear - selectionStyle = .none - initialViews() - } - - 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 - } - - // MARK: - Setup - - private func initialViews() { - contentView.addSubview(checkButton) - contentView.addSubview(label) - contentView.addSubview(line) - contentView.addSubview(signInFirstTipLabel) - - checkButton.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 20, height: 20)) - make.right.equalToSuperview().offset(-24) - make.centerY.equalToSuperview() - } - - label.snp.makeConstraints { make in - make.left.equalToSuperview().offset(24) - make.top.equalToSuperview().offset(24) - make.bottom.equalToSuperview().offset(-24) - make.right.lessThanOrEqualTo(checkButton.snp.left).offset(-16) - } - - line.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalToSuperview() - make.height.equalTo(0.5) - } - - signInFirstTipLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.right.equalToSuperview().offset(-24) - } - - label.textColor = .c.ctpn - line.backgroundColor = .c.con - } - - // MARK: - Public Methods - - override func config(data: SelectCommonModelProtocol) { - label.text = data.displayStr() - } - - override func configSelect(selected: Bool) { - checkButton.isHidden = !selected - } - - func configSignInFirstMode(_ mode: Bool) { - signInFirstMode = mode - if mode { - label.textColor = .c.cttn - signInFirstTipLabel.isHidden = false - } else { - label.textColor = .c.ctpn - signInFirstTipLabel.isHidden = true - } - } -} diff --git a/crush/Crush/Src/Components/UI/Placeholder/PlaceholderView.swift b/crush/Crush/Src/Components/UI/Placeholder/PlaceholderView.swift deleted file mode 100644 index 0658e3a..0000000 --- a/crush/Crush/Src/Components/UI/Placeholder/PlaceholderView.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// PlaceholderView.swift -// Crush -// -// Created by Leon on 2025/7/14. -// - -import SnapKit -import UIKit - -/// A reusable placeholder view with an image and text label -class PlaceholderView: UIView { - // MARK: - Properties - - lazy var emptyImage: UIImage? = { - UIImage(named: "empty_placeholder_icon") - }() - - var viewStartOffset: CGFloat = 0 - var startY: CGFloat = 0 - - private let placeholderImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.tintColor = .gray // Adjust tint for template images - return imageView - }() - - private let placeholderLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.textColor = .gray - label.font = .systemFont(ofSize: 16, weight: .regular) - label.numberOfLines = 0 // Allow multiple lines - return label - }() - - // MARK: - Initialization - - init(text: String, viewStartOffset: CGFloat = 0) { - super.init(frame: .zero) - self.viewStartOffset = viewStartOffset - setupViews(image: emptyImage) - updateViewOffset(text: text) - } - - init(text: String, startY: CGFloat = 0) { - super.init(frame: .zero) - self.startY = startY - setupViews(image: emptyImage) - updateViewStartY(text: text) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - private func setupViews(image: UIImage?) { - isUserInteractionEnabled = false - backgroundColor = .clear // Adjust as needed - - // Add subviews - addSubview(placeholderImageView) - addSubview(placeholderLabel) - - // Configure content - placeholderImageView.image = image - } - - // MARK: - Public Methods - - func updateViewOffset(text: String) { - placeholderLabel.text = text - placeholderImageView.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.centerY.equalToSuperview().offset((-40 + viewStartOffset) * 0.5) // Offset to position image above text - make.size.equalTo(CGSize(width: 183, height: 135)) - } - - placeholderLabel.snp.makeConstraints { make in - make.top.equalTo(placeholderImageView.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - } - } - - func updateViewStartY(text: String) { - placeholderLabel.text = text - placeholderImageView.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalToSuperview().offset(startY) - make.width.height.equalTo(100) // Adjust size as needed - } - - placeholderLabel.snp.makeConstraints { make in - make.top.equalTo(placeholderImageView.snp.bottom).offset(16) - make.left.right.equalToSuperview().inset(16) - } - } -} - -// MARK: - UIView Extension - -extension UIView { - /// Adds a placeholder view with an image and text to the view - /// - Parameters: - /// - text: The placeholder text - /// - Returns: The created PlaceholderView instance - @discardableResult - func showEmpty(text: String, viewStartOffset: CGFloat = 0) -> PlaceholderView { - // Remove any existing placeholder views - subviews.filter { $0 is PlaceholderView }.forEach { $0.removeFromSuperview() } - - // Create and configure placeholder view - let placeholderView = PlaceholderView(text: text, viewStartOffset: viewStartOffset) - addSubview(placeholderView) - - // Setup Auto Layout - placeholderView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - placeholderView.setNeedsDisplay() - placeholderView.layoutIfNeeded() - - return placeholderView - } - - @discardableResult - func showStartYEmpty(text: String, startY: CGFloat = 200) -> PlaceholderView { - // Remove any existing placeholder views - subviews.filter { $0 is PlaceholderView }.forEach { $0.removeFromSuperview() } - - // Create and configure placeholder view - let placeholderView = PlaceholderView(text: text, startY: startY) - addSubview(placeholderView) - - // Setup Auto Layout - placeholderView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - return placeholderView - } - - /// Removes any existing placeholder view - func removeEmpty() { - subviews.filter { $0 is PlaceholderView }.forEach { $0.removeFromSuperview() } - } - - func hideEmpty(){ - removeEmpty() - } - - // MARK: - Public - func setupEmpty(empty: Bool, startY: CGFloat? = 0, msg: String?){ - if(empty){ - if let y = startY, y > 0{ - showStartYEmpty(text: msg ?? "", startY: y) - }else{ - showEmpty(text: msg ?? "") - } - }else{ - removeEmpty() - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Appearance/FSPopoverView+Appearance.swift b/crush/Crush/Src/Components/UI/Popover/Appearance/FSPopoverView+Appearance.swift deleted file mode 100644 index 68f1c4e..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Appearance/FSPopoverView+Appearance.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// FSPopoverView+Appearance.swift -// FSPopoverView -// -// Created by Sheng on 2023/12/2. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit -import Foundation - -public extension FSPopoverView { - - static func fs_appearance() -> FSPopoverViewAppearance { - return FSPopoverViewAppearance.shared - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Appearance/FSPopoverViewAppearance.swift b/crush/Crush/Src/Components/UI/Popover/Appearance/FSPopoverViewAppearance.swift deleted file mode 100644 index f849eae..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Appearance/FSPopoverViewAppearance.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// FSPopoverViewAppearance.swift -// FSPopoverView -// -// Created by Sheng on 2023/12/2. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit -import Foundation - -public final class FSPopoverViewAppearance { - - // MARK: Properties/Public - - public var showsArrow: Bool - public var showsDimBackground: Bool - public var cornerRadius: CGFloat - public var arrowSize: CGSize - public var borderWidth: CGFloat - public var borderColor: UIColor? - public var shadowColor: UIColor? - public var shadowRadius: CGFloat - public var shadowOpacity: Float - public var backgroundColor: UIColor? - // list - public var spacing: CGFloat - public var textFont: UIFont - public var textColor: UIColor? - public var separatorInset: UIEdgeInsets - public var separatorColor: UIColor? - public var highlightedColor: UIColor? - - // MARK: Initialization - - static let shared = FSPopoverViewAppearance() - - private init() { - showsArrow = true - showsDimBackground = false - cornerRadius = 8.0 - arrowSize = .init(width: 22.0, height: 10.0) - borderWidth = 1.0 - shadowRadius = 3.0 - shadowOpacity = 0.68 - spacing = 6.0 - textFont = .systemFont(ofSize: 18.0) - separatorInset = .init(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) - // colors - if #available(iOS 13.0, *) { - borderColor = UIColor(dynamicProvider: { trait in - if trait.userInterfaceStyle == .dark { - return .inner.color(hexed: "423E55") ?? .black - } - return .inner.color(hexed: "CFCFCF") ?? .black - }) - shadowColor = UIColor(dynamicProvider: { trait in - if trait.userInterfaceStyle == .dark { - return .inner.color(hexed: "E8E8E8") ?? .black - } - return .inner.color(hexed: "696969") ?? .black - }) - backgroundColor = UIColor(dynamicProvider: { trait in - if trait.userInterfaceStyle == .dark { - return .inner.color(hexed: "1A172B") ?? .black - } - return .white - }) - textColor = UIColor(dynamicProvider: { trait in - if trait.userInterfaceStyle == .dark { - return .inner.color(hexed: "E8E8E8") ?? .black - } - return .black - }) - separatorColor = UIColor(dynamicProvider: { trait in - if trait.userInterfaceStyle == .dark { - return .inner.color(hexed: "4E4A64") ?? .black - } - return .lightGray - }) - highlightedColor = UIColor(dynamicProvider: { trait in - if trait.userInterfaceStyle == .dark { - return .inner.color(hexed: "#322F47") ?? .black - } - return .black.withAlphaComponent(0.1) - }) - } else { - borderColor = .inner.color(hexed: "CFCFCF") ?? .black - shadowColor = .inner.color(hexed: "696969") ?? .black - backgroundColor = .white - textColor = .black - separatorColor = .lightGray - highlightedColor = .black.withAlphaComponent(0.1) - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/CLPopoverListView.swift b/crush/Crush/Src/Components/UI/Popover/CLPopoverListView.swift deleted file mode 100644 index f17e43b..0000000 --- a/crush/Crush/Src/Components/UI/Popover/CLPopoverListView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// CLPopoverView.swift -// Crush -// -// Created by Leon on 2025/7/28. -// -import UIKit -extension FSPopoverView { - func clStyle() { - showsArrow = false - // titleFont = .t.tll - } -} - -class CLPopoverListView: FSPopoverListView { - var customBackGroundView: UIView = UIView() - - override init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { - super.init(scrollDirection: scrollDirection) - - showsArrow = false - cornerRadius = 16 - shadowColor = nil - shadowRadius = 0 - shadowOpacity = 0 - borderWidth = 0 - FSPopoverView.fs_appearance().backgroundColor = .c.csfn - // ❌backgroundColor = .c.csfn - } - - override func backgroundView(for popoverView: FSPopoverView) -> UIView? { - customBackGroundView.backgroundColor = .c.csfn - return customBackGroundView - } - - override func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets { - return .init(top: 10.0, left: 16.0, bottom: 10.0, right: 16.0) - } -} - -extension CLPopoverListView { - /// 通用popover Crushlevel样式 - static func setupCLStyle() { - FSPopoverViewAppearance.shared.spacing = 16 - FSPopoverViewAppearance.shared.textFont = .t.tll - FSPopoverViewAppearance.shared.textColor = .white - FSPopoverViewAppearance.shared.borderWidth = 0 - FSPopoverViewAppearance.shared.cornerRadius = 16 - FSPopoverViewAppearance.shared.backgroundColor = .c.csfn - } - - func setupDeletePopover(_ rect: CGRect, inView: UIView, block: (() -> Void)?) { - var itemsNew: [CLPopoverListTextItem] = [CLPopoverListTextItem]() - - let item = CLPopoverListTextItem() - item.image = MWIconFont.image(fromIcon: .iconDelete, size: .init(width: 20, height: 20), color: .white) // .white - item.title = "Delete" - item.setupCLStyle() - item.updateLayout()// 必需 - item.selectedHandler = { item in - guard let item = item as? CLPopoverListTextItem else { - return - } - dlog(item.title ?? "") - block?() - } - itemsNew.append(item) - - items = itemsNew - dismissWhenSelected = true - - present(fromRect: rect, in: inView) - } - - /// 🔥通用调用Popover Menu方法 - public func setupCommonPopover(_ rect: CGRect, inView: UIView, items: [CLPopoverListTextItem], block: (() -> Void)?) { - self.items = items - dismissWhenSelected = true - present(fromRect: rect, in: inView) - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/FSPopoverView.swift b/crush/Crush/Src/Components/UI/Popover/FSPopoverView.swift deleted file mode 100644 index c7ce0c5..0000000 --- a/crush/Crush/Src/Components/UI/Popover/FSPopoverView.swift +++ /dev/null @@ -1,1021 +0,0 @@ -// -// FSPopoverView.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/2. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -open class FSPopoverView: UIView { - - // MARK: ArrowDirection - - public enum ArrowDirection { - case up, down, left, right - } - - // MARK: Properties/Open - - /// The object that acts as the data source of the popover view. - /// - /// * The data source must adopt the FSPopoverViewDataSource protocol. - /// * The data source is not retained. - /// * A reload request will be set when this property is set. - /// - weak open var dataSource: FSPopoverViewDataSource? { - didSet { - setNeedsReload() - } - } - - /// The transitioning delegate object is a custom object that you provide - /// and that conforms to the FSPopoverViewAnimatedTransitioning protocol. - /// - /// * You can use this property to custom the transition animation of popover view. - /// * A default transitioning delegate is set for popover view. - /// * This object is not retained. - /// - weak open var transitioningDelegate: FSPopoverViewAnimatedTransitioning? { - didSet { - if let scale = scaleTransition, scale !== transitioningDelegate { - scaleTransition = nil - } - } - } - - /// The direction of the popover's arrow. - /// - /// * You can change this property even though the popover view is displaying. - /// * A reload request will be set when this property is changed. - /// * See ``autosetsArrowDirection`` property for more information about this - /// property. - /// - open var arrowDirection = FSPopoverView.ArrowDirection.up { - didSet { - if arrowDirection != oldValue, !autosetsArrowDirection, !isReloading { - setNeedsReload() - } - } - } - - /// Whether to set the arrow direction automatically. - /// Defaults to true. - /// - /// * When this property is true, the popover view will determine the direction - /// of the arrow and update the ``arrowDirection`` property automatically. - /// * When this property is false, The popover view will calculate it's position - /// according to the ``arrowDirection`` property, and the ``arrowDirection`` property - /// will never be changed by inside. - /// * A reload request will be set when this property is changed. - /// - /// - Important - /// * When this property is true, the popover view will calculate the appropriate - /// position based on the size of it's container and the position of the arrow. - /// So, when you set this property to false, you should ensure that there is enough - /// space in the container for the popover view to appear, otherwise the popover - /// view may be in the wrong place. - /// - open var autosetsArrowDirection = true { - didSet { - if autosetsArrowDirection != oldValue { - setNeedsReload() - } - } - } - - // MARK: Properties/Public - - /// The location of the arrow's vertex. - /// Defaults to (0, 0). - /// - /// * This point is in the coordinate system of `containerView`. - /// * This point will be recalculated on reload operation. - /// * The value of ``showsArrow`` has no effect on this property. - /// - final public private(set) var arrowPoint: CGPoint = .zero - - /// The container view displaying the popover view. - /// This view will be created when the popover view needs to be displayed. - /// - /// If popover view is displaying in a specified view, the specified view will be the superview - /// of the container view. Otherwise, a window will be created automatically inside the popover - /// view as the superview of the container view, and this window will be the same size as the - /// current screen. - /// - /// The popover view is added to the container view. - /// The view hierarchy is: - /// ``` - /// specified view / window - /// - container view (same size as specified view / window) - /// - dim background view (same size as container view) - /// - user interaction view (same size as container view) - /// - popover view - /// - popover container view - /// - background view - /// - content view (from data source) - /// ``` - /// - final weak public private(set) var containerView: UIView? - - /// Arrow will be hidden when this property is set to false. - /// Default value see `FSPopoverViewAppearance`. - /// - /// * A reload request will be set when this property is changed. - /// - final public var showsArrow: Bool { - didSet { - if showsArrow != oldValue { - setNeedsReload() - } - } - } - - /// Whether needs to show a dim background on container view. - /// Default value see ``FSPopoverViewAppearance``. - final public var showsDimBackground: Bool { - didSet { - dimBackgroundView.isHidden = !showsDimBackground - } - } - - /// The corner radius of the popover view. - /// Default value see ``FSPopoverViewAppearance``. - /// - /// * A reload request will be set when this property is set. - /// - final public override var cornerRadius: CGFloat { - didSet { - if cornerRadius != oldValue { - setNeedsReload() - } - } - } - - /// The size of the arrow. - /// Default value see ``FSPopoverViewAppearance``. - /// - /// * A reload request will be set when this property is set. - /// - final public var arrowSize: CGSize { - didSet { - if arrowSize != oldValue { - setNeedsReload() - } - } - } - - /// The width of the popover view border. - /// Default value see ``FSPopoverViewAppearance``. - /// - /// * A reload request will be set when this property is changed. - /// - final public override var borderWidth: CGFloat { - didSet { - if borderWidth != oldValue { - setNeedsReload() - } - } - } - - /// The color of the popover view border. - /// Default value see ``FSPopoverViewAppearance``. - /// - /// * A reload request will be set when this property is set. - /// - final public override var borderColor: UIColor? { - didSet { - setNeedsReload() - } - } - - /// The color of the popover view shadow. - /// Default value see ``FSPopoverViewAppearance``. - /// - /// * A reload request will be set when this property is set. - /// - final public var shadowColor: UIColor? { - didSet { - setNeedsReload() - } - } - - /// The radius of the popover view shadow. - /// Default value see ``FSPopoverViewAppearance``. - /// - /// * A reload request will be set when this property is changed. - /// - final public var shadowRadius: CGFloat { - didSet { - if shadowRadius != oldValue { - setNeedsReload() - } - } - } - - /// The opacity of the popover view shadow. - /// The value in this property must be in the range 0.0 (transparent) to 1.0 (opaque). - /// Default value see ``FSPopoverViewAppearance``. - /// - /// * A reload request will be set when this property is changed. - /// - final public var shadowOpacity: Float { - didSet { - if shadowOpacity != oldValue { - setNeedsReload() - } - } - } - - // MARK: Properties/Override - - /// It's objected to use this property to set the background color of popover view. - /// Use ``backgroundView`` of ``dataSource`` instead. - @available(*, unavailable) - final public override var backgroundColor: UIColor? { - get { return nil } - set {} - } - - // MARK: Properties/Private - - private var needsReload = false - - private var isFreezing = false - - private var isReloading = false - - private let delegateRouter = FSPopoverViewDelegateRouter() - - weak private var backgroundView: UIView? - - weak private var contentView: UIView? - - /// ``backgroundView`` and ``contentView`` will be added to this view. - /// - /// * This view will be the same size as the popover view. - /// - private lazy var popoverContainerView: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - - weak private var borderLayer: CALayer? - weak private var shadowLayer: CALayer? - - /// Size of `containerView`. - private var containerSize: CGSize = .zero - - /// This rect is in the coordinate system of ``containerView``. - private var arrowReferRect: CGRect = .zero - - /// If there is no specified view to display popover view, - /// this window will be created. - private var displayWindow: UIWindow? - - /// The dim background on container view. - private lazy var dimBackgroundView: UIView = { - let view = UIView() - view.isHidden = true - view.backgroundColor = .init(white: 0.0, alpha: 0.25) - view.isUserInteractionEnabled = false - return view - }() - - /// The view that receives user interaction. - private lazy var userInteractionView: UIView = { - let view = UIView() - view.backgroundColor = .clear - view.isUserInteractionEnabled = true - do { - let tap = UITapGestureRecognizer(target: self, action: #selector(p_handleTap)) - tap.delegate = delegateRouter - view.addGestureRecognizer(tap) - } - return view - }() - - /// The default value of transitioning delegate. - private var scaleTransition: FSPopoverViewTransitionScale? - - // MARK: Initialization - - public init() { - - // appearance - let appearance = FSPopoverViewAppearance.shared - showsArrow = appearance.showsArrow - showsDimBackground = appearance.showsDimBackground -// cornerRadius = appearance.cornerRadius - arrowSize = appearance.arrowSize -// borderWidth = appearance.borderWidth -// borderColor = appearance.borderColor - shadowColor = appearance.shadowColor - shadowRadius = appearance.shadowRadius - shadowOpacity = appearance.shadowOpacity - - super.init(frame: .zero) - cornerRadius = appearance.cornerRadius - borderWidth = appearance.borderWidth - borderColor = appearance.borderColor - p_didInitialize() - } - - @available(*, unavailable) - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Methods/Override - - override open func layoutSubviews() { - super.layoutSubviews() - reloadDataIfNeeded() - } - - open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if #unavailable(iOS 17) { - setNeedsReload() - } - } - - @available(*, unavailable) - open override class func appearance() -> Self { - return super.appearance() - } - - @available(*, unavailable) - open override class func appearance(for trait: UITraitCollection) -> Self { - return super.appearance(for: trait) - } - - @available(*, unavailable) - open override class func appearance(whenContainedInInstancesOf containerTypes: [UIAppearanceContainer.Type]) -> Self { - return super.appearance(whenContainedInInstancesOf: containerTypes) - } - - @available(*, unavailable) - open override class func appearance(for trait: UITraitCollection, whenContainedInInstancesOf containerTypes: [UIAppearanceContainer.Type]) -> Self { - return super.appearance(for: trait, whenContainedInInstancesOf: containerTypes) - } - - // MARK: Methods/Open - - /// Tells the popover view to reload all of its contents. - /// - /// - Requires - /// * Subclasses **must** call `super.setNeedsReload()` when overriding this method, - /// otherwise some bugs may occur. - /// - /// - Note - /// * This method makes a note of the request and returns immediately. This method - /// does not force an immediate reload, all of the contents will reload in view's - /// next layout update cycle. This behavior allows you to consolidate all of your - /// content reloads to one layout update cycle, which is usually better for performance. - /// * You should call this method on the main thread. - /// - open func setNeedsReload() { - p_mainThreadCheck() - needsReload = true - setNeedsLayout() - } - - /// Reload the contents immediately if the reload operation is pending. - /// - /// - Requires - /// * Subclasses **must** call `super.reloadDataIfNeeded()` when overriding this method, - /// otherwise some bugs may occur. - /// - /// - Note - /// * Use this method to force the popover view to reload its contents immediately, but if - /// the reload operation is not pending, this method exits without modifying the contents - /// or calling any content-related callbacks. - /// * You should call this method on the main thread. - /// - open func reloadDataIfNeeded() { - p_mainThreadCheck() - if needsReload { - reloadData() - } - } - - /// Reload the contents immediately, even without any reload requests. - /// - /// - Requires - /// * Subclasses **must** call `super.reloadData()` when overriding this method, - /// otherwise some bugs may occur. - /// - /// - Note - /// * Calling this method will not have any animation, even if some contents are - /// changed, like size of content and direction of arrow and so on. - /// * You should call this method on the main thread. - /// - open func reloadData() { - p_mainThreadCheck() - isReloading = true - p_reloadData() - isReloading = false - } - - /// Get the transition context for the specified scene. - open func transitionContext(for scene: FSPopoverViewTransitionContext.Scene) -> FSPopoverViewTransitionContext { - return FSPopoverViewTransitionContext(scene: scene, popoverView: self, dimBackgroundView: dimBackgroundView) - } - - /// Presents the popover and anchors it to the specified view. - open func present(fromView view: UIView, - displayIn specifiedView: UIView? = nil, - animated: Bool = true, - completion: (() -> Void)? = nil) { - p_present(from: view.frame, - in: view.superview, - displayIn: specifiedView, - animated: animated, - completion: completion) - } - - /// Presents the popover and anchors it to the specified location. - /// - Parameters: - /// - view: The view containing the point. - open func present(fromPoint point: CGPoint, - in view: UIView? = nil, - displayIn specifiedView: UIView? = nil, - animated: Bool = true, - completion: (() -> Void)? = nil) { - p_present(from: .init(origin: point, size: .zero), - in: view, - displayIn: specifiedView, - animated: animated, - completion: completion) - } - - /// Presents the popover and anchors it to the specified rect. - /// - Parameters: - /// - view: The view containing the rectangle. - open func present(fromRect rect: CGRect, - in view: UIView? = nil, - displayIn specifiedView: UIView? = nil, - animated: Bool = true, - completion: (() -> Void)? = nil) { - p_present(from: rect, - in: view, - displayIn: specifiedView, - animated: animated, - completion: completion) - } - - /// Presents the popover and anchors it to the specified bar item. - open func present(fromBarItem barItem: UIBarItem, animated: Bool = true, completion: (() -> Void)? = nil) { - guard let view = barItem.value(forKey: "view") as? UIView else { - #if DEBUG - fatalError("The value of UIBarItem has been changed, this method can not be used anymore.") - #else - return - #endif - } - p_present(from: view.frame, - in: view.superview, - animated: animated, - completion: completion) - } - - /// Dismiss popover view. - open func dismiss(animated: Bool = true, isSelection: Bool = false, completion: (() -> Void)? = nil) { - p_dismiss(animated: animated, isSelection: isSelection, completion: completion) - } -} - -// MARK: - Methods/Private - -private extension FSPopoverView { - - /// Invoked after initialization. - func p_didInitialize() { - - popoverContainerView.backgroundColor = .clear - - delegateRouter.gestureRecognizerShouldBeginHandler = { [unowned self] gestureRecognizer in - if let view = gestureRecognizer.view, view === self.userInteractionView { - if self.isFreezing { - return true - } - let location = gestureRecognizer.location(in: view) - return !self.frame.contains(location) - } - return false - } - - if #available(iOS 17, *) { - registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, previousTraitCollection: UITraitCollection) in - self.setNeedsReload() - } - } - - addSubview(popoverContainerView) - popoverContainerView.inner.makeMarginConstraints(to: self) - - scaleTransition = FSPopoverViewTransitionScale() - transitioningDelegate = scaleTransition - } - - func p_mainThreadCheck() { - #if DEBUG - if !Thread.isMainThread { - fatalError("You must call this method on the main thread.") - } - #endif - } - - func p_getArrowSize() -> CGSize { - guard showsArrow else { - return .zero - } - var size = arrowSize - size.width = max(size.width, 0) - size.height = max(size.height, 0) - return size - } - - func p_createContainerView() -> UIView { - let view = UIView() - view.backgroundColor = .clear - return view - } - - func p_createDisplayWindow() -> UIWindow { - let window = UIWindow() - window.windowLevel = .statusBar - 1 - window.backgroundColor = .clear - if #available(iOS 13.0, *), let scene = UIApplication.shared.connectedScenes.filter({ $0.activationState == .foregroundActive }).first as? UIWindowScene { - window.windowScene = scene - } - return window - } - - func p_prepareForDisplaying() { - // freezes the popover view before the popover view finishes displaying operation. - isFreezing = true - alpha = 1.0 - transform = .identity - removeFromSuperview() - containerSize = .zero - containerView?.removeFromSuperview() - dimBackgroundView.alpha = 1.0 - dimBackgroundView.transform = .identity - p_destroyDisplayWindow() - } - - func p_setContainerView(_ view: UIView) { - - containerView = view - - dimBackgroundView.removeFromSuperview() - view.addSubview(dimBackgroundView) - dimBackgroundView.inner.makeMarginConstraints(to: view) - - userInteractionView.removeFromSuperview() - view.addSubview(userInteractionView) - userInteractionView.inner.makeMarginConstraints(to: view) - } - - func p_destroyDisplayWindow() { - displayWindow?.isHidden = true - displayWindow?.subviews.forEach { $0.removeFromSuperview() } - displayWindow = nil - } - - /// Reset all contents to default values. - func p_resetContents() { - borderLayer?.removeFromSuperlayer() - shadowLayer?.removeFromSuperlayer() - contentView?.removeFromSuperview() - backgroundView?.removeFromSuperview() - borderLayer = nil - shadowLayer = nil - contentView = nil - backgroundView = nil - } - - /// Reload all contents and recalculate the position and size of the popover view. - func p_reloadData() { - - p_mainThreadCheck() - - needsReload = false - - // clear old contents - p_resetContents() - - guard containerSize.width > 0, containerSize.height > 0 else { - return - } - - // container safe area insets - let safeAreaInsets = dataSource?.containerSafeAreaInsets(for: self) ?? .zero - let safeContainerRect = CGRect(origin: .zero, size: containerSize).inset(by: safeAreaInsets) - - // content size - let realContentSize = dataSource?.contentSize(for: self) ?? .zero - let contentSize: CGSize - - // arrow size - let arrowSize = p_getArrowSize() - - // arrow direction - do { - let horizontalContentSize: CGSize = { - var size = CGSize(width: cornerRadius * 2 + 10.0, height: arrowSize.height + cornerRadius * 2 + 10.0) - if realContentSize.width > size.width { - size.width = realContentSize.width - } - if realContentSize.height > size.height { - size.height = realContentSize.height - } - return size - }() - let verticalContentSize: CGSize = { - var size = CGSize(width: arrowSize.width + cornerRadius * 2 + 10.0, height: cornerRadius * 2 + 10.0) - if realContentSize.width > size.width { - size.width = realContentSize.width - } - if realContentSize.height > size.height { - size.height = realContentSize.height - } - return size - }() - if autosetsArrowDirection { - let referRect = arrowReferRect.insetBy(dx: -arrowSize.height, dy: -arrowSize.height) - let topSpace = CGSize(width: safeContainerRect.width, height: max(0.0, referRect.minY - safeContainerRect.minY)) - let leftSpace = CGSize(width: max(0.0, referRect.minX - safeContainerRect.minX), height: safeContainerRect.height) - let bottomSpace = CGSize(width: safeContainerRect.width, height: max(0.0, safeContainerRect.maxY - referRect.maxY)) - let rightSpace = CGSize(width: max(0.0, safeContainerRect.maxX - referRect.maxX), height: safeContainerRect.height) - // priority: up > down > left > right - if bottomSpace.width >= verticalContentSize.width && bottomSpace.height >= verticalContentSize.height { - contentSize = verticalContentSize - arrowDirection = .up - } else if topSpace.width >= verticalContentSize.width && topSpace.height >= verticalContentSize.height { - contentSize = verticalContentSize - arrowDirection = .down - } else if rightSpace.width >= horizontalContentSize.width && rightSpace.height >= horizontalContentSize.height { - contentSize = horizontalContentSize - arrowDirection = .left - } else if leftSpace.width >= horizontalContentSize.width && leftSpace.height >= horizontalContentSize.height { - contentSize = horizontalContentSize - arrowDirection = .right - } else { - // `up` will be set if there is not enough space to show popover view. - arrowDirection = .up - contentSize = verticalContentSize - #if DEBUG - let message = """ - ⚠️ Not enough space to show popover view, \ - you may need to check if the popover view is showing on the wrong view. ⚠️ - """ - print(message) - #endif - } - } else { - switch arrowDirection { - case .up, .down: - contentSize = verticalContentSize - case .left, .right: - contentSize = horizontalContentSize - } - } - } - - // arrow point - // This point is in the coordinate system of container view. - do { - var point = CGPoint.zero - switch arrowDirection { - case .up: - point.x = arrowReferRect.midX - point.y = arrowReferRect.maxY - case .down: - point.x = arrowReferRect.midX - point.y = arrowReferRect.minY - case .left: - point.x = arrowReferRect.maxX - point.y = arrowReferRect.midY - case .right: - point.x = arrowReferRect.minX - point.y = arrowReferRect.midY - } - // If the point is on the edge, a little adjustment is required for improving the visual effect. - let arrowPointSafeRect: CGRect = { - var rect = safeContainerRect.insetBy(dx: cornerRadius, dy: cornerRadius) - rect = rect.insetBy(dx: 3.0, dy: 3.0) - rect = rect.insetBy(dx: arrowSize.width / 2, dy: arrowSize.width / 2) - return rect - }() - if !arrowPointSafeRect.contains(point) { - switch arrowDirection { - case .up, .down: - if point.x < arrowPointSafeRect.minX { - point.x = arrowPointSafeRect.minX - } - if point.x > arrowPointSafeRect.maxX { - point.x = arrowPointSafeRect.maxX - } - case .left, .right: - if point.y < arrowPointSafeRect.minY { - point.y = arrowPointSafeRect.minY - } - if point.y > arrowPointSafeRect.maxY { - point.y = arrowPointSafeRect.maxY - } - } - } - arrowPoint = point - } - - // The frame of the popover view in the container view. - let frame: CGRect = { - let size: CGSize = { - var width: CGFloat = contentSize.width, height: CGFloat = contentSize.height - switch arrowDirection { - case .up, .down: - height += arrowSize.height - case .left, .right: - width += arrowSize.height - } - return .init(width: width, height: height) - }() - var origin = CGPoint.zero - switch arrowDirection { - case .up, .down: - origin.x = { - var x = arrowPoint.x - size.width / 2 - if arrowPoint.x <= safeContainerRect.midX { - x = max(x, safeContainerRect.minX) - } - if arrowPoint.x > safeContainerRect.midX { - x = min(x, safeContainerRect.maxX - size.width) - } - return x - }() - case .left, .right: - origin.y = { - var y = arrowPoint.y - size.height / 2 - if arrowPoint.y <= safeContainerRect.midY { - y = max(y, safeContainerRect.minY) - } - if arrowPoint.y > safeContainerRect.midY { - y = min(y, safeContainerRect.maxY - size.height) - } - return y - }() - } - switch arrowDirection { - case .up: - origin.y = arrowPoint.y - case .down: - origin.y = arrowPoint.y - size.height - case .left: - origin.x = arrowPoint.x - case .right: - origin.x = arrowPoint.x - size.width - } - return .init(origin: origin, size: size) - }() - self.frame = frame - - // background view - if let view = dataSource?.backgroundView(for: self) { - popoverContainerView.addSubview(view) - backgroundView = view - view.inner.makeMarginConstraints(to: popoverContainerView) - } - - // content view - if let view = dataSource?.contentView(for: self) { - popoverContainerView.addSubview(view) - contentView = view - var frame = CGRect.zero - frame.size = realContentSize - switch arrowDirection { - case .up: - frame.origin.x = (contentSize.width - realContentSize.width) / 2 - frame.origin.y = arrowSize.height + (contentSize.height - realContentSize.height) / 2 - case .left: - frame.origin.x = arrowSize.height + (contentSize.width - realContentSize.width) / 2 - frame.origin.y = (contentSize.height - realContentSize.height) / 2 - case .down, .right: - frame.origin.x = (contentSize.width - realContentSize.width) / 2 - frame.origin.y = (contentSize.height - realContentSize.height) / 2 - } - view.frame = frame - } - - // draw popover view - do { - let arrowPointInPopover = CGPoint(x: arrowPoint.x - frame.minX, y: arrowPoint.y - frame.minY) - - var context = FSPopoverDrawContext() - context.showsArrow = showsArrow - context.arrowSize = arrowSize - context.arrowPoint = arrowPointInPopover - context.arrowDirection = arrowDirection - context.cornerRadius = cornerRadius - context.contentSize = contentSize - context.popoverSize = frame.size - context.borderWidth = borderWidth - context.borderColor = borderColor - context.shadowColor = shadowColor - context.shadowRadius = shadowRadius - context.shadowOpacity = shadowOpacity - - let drawer = FSPopoverDrawer(context: context) - - // mask - do { - let path = drawer.generatePath() - let maskLayer = CAShapeLayer() - maskLayer.path = path.cgPath - popoverContainerView.layer.mask = maskLayer - } - - // shadow - if let image = drawer.generateShadowImage() { - let layer = CALayer() - layer.contents = image.cgImage - self.layer.addSublayer(layer) - self.shadowLayer = layer - layer.frame.size = image.size - layer.frame.origin.x = (frame.width - image.size.width) / 2 - layer.frame.origin.y = (frame.height - image.size.height) / 2 - } - - // border - if let image = drawer.generateBorderImage() { - let layer = CALayer() - layer.contents = image.cgImage - self.layer.addSublayer(layer) - self.borderLayer = layer - layer.frame.size = image.size - layer.frame.origin.x = (frame.width - image.size.width) / 2 - layer.frame.origin.y = (frame.height - image.size.height) / 2 - } - } - } - - func p_present(from rect: CGRect, - in view: UIView? = nil, - displayIn specifiedView: UIView? = nil, - animated: Bool = true, - completion: (() -> Void)? = nil) { - - p_mainThreadCheck() - p_prepareForDisplaying() - - let displayView: UIView = { - /// NOTE: - /// When displaying in the UIScrollView, container view can not get the correct size. - /// So ignore scroll view here. - if let view = specifiedView, !(view is UIScrollView), view.bounds.width > 0, view.bounds.height > 0.0 { - return view - } - let window = p_createDisplayWindow() - window.bounds = .init(origin: .zero, size: UIScreen.main.bounds.size) - window.isHidden = false - displayWindow = window - return window - }() - - arrowReferRect = { - if let view = view { - return view.convert(rect, to: displayView) - } - return rect - }() - - let containerView = p_createContainerView() - do { - p_setContainerView(containerView) - displayView.addSubview(containerView) - containerView.inner.makeMarginConstraints(to: displayView) - displayView.layoutIfNeeded() - } - - containerSize = containerView.bounds.size - - setNeedsReload() - - // This operation will cause the method `layoutSubviews()` to be called, and then - // the method `reloadDataIfNeeded()` will be called, so it's unnecessary to call - // `reloadDataIfNeeded()` explicitly here. - containerView.addSubview(self) - // Layout the popover view for the animation of the next step. - containerView.layoutIfNeeded() - - let completedHandler: (() -> Void) = { [unowned self] in - self.isFreezing = false - completion?() - } - - if animated, let transitioning = transitioningDelegate { - let context = transitionContext(for: .present) - context.onDidCompleteTransition = completedHandler - transitioning.animateTransition(transitionContext: context) - } else { - completedHandler() - } - } - - func p_dismiss(animated: Bool = true, isSelection: Bool = false, completion: (() -> Void)? = nil) { - - p_mainThreadCheck() - - // Can not do anything when the popover view begins disappearing. - isFreezing = true - - containerView?.isUserInteractionEnabled = false - - let completedHandler: (() -> Void) = { [unowned self] in - self.isFreezing = false - self.removeFromSuperview() - self.containerView?.removeFromSuperview() - self.p_destroyDisplayWindow() - completion?() - } - - if animated, let transitioning = transitioningDelegate { - let context = transitionContext(for: .dismiss(isSelection)) - context.onDidCompleteTransition = completedHandler - transitioning.animateTransition(transitionContext: context) - } else { - completedHandler() - } - } -} - -// MARK: - Methods/Actions - -private extension FSPopoverView { - - @objc func p_handleTap() { - guard - !isFreezing, // Can not do anything while the popover view is freezing. - dataSource?.popoverViewShouldDismissOnTapOutside(self) ?? true - else { - return - } - p_dismiss() - } -} - -// MARK: - Methods/Public - -public extension FSPopoverView { - - /// Call this method if you want to bring the popover view to the front of - /// the view displaying the popover view. - func moveToFront() { - guard - let containerView = containerView, - let superview = containerView.superview - else { - return - } - superview.bringSubviewToFront(containerView) - } - - /// Call this method to get the maximum size in the direction. - /// - /// Sometimes, the size of your content view may need to be adjusted to fit the popover view. - /// Such as your content view is a table view with a lot of cells, and the popover view doesn't - /// have enough space for the table view to show all of the cells, this requires the data source - /// object to return a proper content size. In this time, you need a size as a reference, and - /// this is where this method in handy. - /// - /// - Important: - /// * Returns nil if the popover view is not yet ready for presenting, so it's recommended that - /// call this method in the content size method of data source object. - /// - func maximumContentSizeOf(direction: FSPopoverView.ArrowDirection) -> CGSize? { - - guard containerSize.width > 0, containerSize.height > 0 else { - return nil - } - - let arrowSize = p_getArrowSize() - let referRect = arrowReferRect.insetBy(dx: -arrowSize.height, dy: -arrowSize.height) - let safeAreaInsets = dataSource?.containerSafeAreaInsets(for: self) ?? .zero - let safeContainerRect = CGRect(origin: .zero, size: containerSize).inset(by: safeAreaInsets) - - switch direction { - case .up: - return CGSize(width: safeContainerRect.width, height: max(0.0, safeContainerRect.maxY - referRect.maxY)) - case .down: - return CGSize(width: safeContainerRect.width, height: max(0.0, referRect.minY - safeContainerRect.minY)) - case .left: - return CGSize(width: max(0.0, safeContainerRect.maxX - referRect.maxX), height: safeContainerRect.height) - case .right: - return CGSize(width: max(0.0, referRect.minX - safeContainerRect.minX), height: safeContainerRect.height) - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/FSPopoverViewDataSource.swift b/crush/Crush/Src/Components/UI/Popover/FSPopoverViewDataSource.swift deleted file mode 100644 index eaf6d9c..0000000 --- a/crush/Crush/Src/Components/UI/Popover/FSPopoverViewDataSource.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// FSPopoverViewDataSource.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/3. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -public protocol FSPopoverViewDataSource: AnyObject { - - func backgroundView(for popoverView: FSPopoverView) -> UIView? - - func contentView(for popoverView: FSPopoverView) -> UIView? - - func contentSize(for popoverView: FSPopoverView) -> CGSize - - func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets - - func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool -} - -/// Optional -public extension FSPopoverViewDataSource { - - func backgroundView(for popoverView: FSPopoverView) -> UIView? { - let view = UIView() - view.backgroundColor = FSPopoverViewAppearance.shared.backgroundColor - return view - } - - func contentView(for popoverView: FSPopoverView) -> UIView? { - return nil - } - - func contentSize(for popoverView: FSPopoverView) -> CGSize { - return .zero - } - - func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets { - return .init(top: 18.0, left: 16.0, bottom: 18.0, right: 16.0)// 10,10,10,10 - } - - func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { - return true - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawContext.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawContext.swift deleted file mode 100644 index fa3382f..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawContext.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// FSPopoverDrawContext.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/10. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -struct FSPopoverDrawContext { - - var showsArrow = true - var arrowSize: CGSize = .zero - var arrowPoint: CGPoint = .zero - var arrowDirection: FSPopoverView.ArrowDirection = .up - - var cornerRadius: CGFloat = 0.0 - var contentSize: CGSize = .zero - var popoverSize: CGSize = .zero - - var borderWidth: CGFloat = 0.0 - var borderColor: UIColor? - - var shadowRadius: CGFloat = 0.0 - var shadowOpacity: Float = 1.0 - var shadowColor: UIColor? - - init () {} -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawDown.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawDown.swift deleted file mode 100644 index 853617c..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawDown.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// FSPopoverDrawDown.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/10. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -struct FSPopoverDrawDown: FSPopoverDrawable { - - func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { - - let popoverRect = CGRect(origin: offset, size: context.popoverSize) - let contentRect = CGRect(origin: offset, size: context.contentSize) - let arrowSize = context.arrowSize.inner.flattedValue - let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue - let cornerRadius = context.cornerRadius.inner.flattedValue - - let path = UIBezierPath() - // top-left - do { - path.move(to: .init(x: popoverRect.minX, y: popoverRect.minY + cornerRadius)) - path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi, - endAngle: .pi * 1.5, - clockwise: true) - } - // top-right - do { - path.addLine(to: .init(x: popoverRect.maxX - cornerRadius, y: popoverRect.minY)) - path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: popoverRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi * 1.5, - endAngle: 0.0, - clockwise: true) - } - // bottom-right - do { - path.addLine(to: .init(x: popoverRect.maxX, y: contentRect.maxY - cornerRadius)) - path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: 0.0, - endAngle: .pi * 0.5, - clockwise: true) - } - // arrow - if context.showsArrow { - let arrowLeftPoint = CGPoint(x: arrowPoint.x - arrowSize.width / 2, y: contentRect.maxY).inner.flattedValue - let arrowRightPoint = CGPoint(x: arrowPoint.x + arrowSize.width / 2, y: contentRect.maxY).inner.flattedValue - path.addLine(to: arrowRightPoint) - do { - let controlPoint1 = CGPoint(x: arrowRightPoint.x - arrowSize.width / 4, y: arrowRightPoint.y).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowPoint.x + arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue - path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) - } - do { - let controlPoint1 = CGPoint(x: arrowPoint.x - arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowLeftPoint.x + arrowSize.width / 4, y: arrowLeftPoint.y).inner.flattedValue - path.addCurve(to: arrowLeftPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) - } - } - // bottom-left - do { - path.addLine(to: .init(x: popoverRect.minX + cornerRadius, y: contentRect.maxY)) - path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: contentRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: .pi * 0.5, - endAngle: .pi, - clockwise: true) - } - // close - path.close() - - return path - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawLeft.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawLeft.swift deleted file mode 100644 index dfa3eac..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawLeft.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// FSPopoverDrawLeft.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/10. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -struct FSPopoverDrawLeft: FSPopoverDrawable { - - func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { - - let popoverRect = CGRect(origin: offset, size: context.popoverSize) - let contentRect = CGRect(origin: .init(x: popoverRect.minX + context.arrowSize.height, y: popoverRect.minY), - size: context.contentSize) - let arrowSize = context.arrowSize.inner.flattedValue - let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue - let cornerRadius = context.cornerRadius.inner.flattedValue - - let path = UIBezierPath() - // top-left - do { - path.move(to: .init(x: contentRect.minX, y: contentRect.minY + cornerRadius)) - path.addArc(withCenter: .init(x: contentRect.minX + cornerRadius, y: contentRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi, - endAngle: .pi * 1.5, - clockwise: true) - } - // top-right - do { - path.addLine(to: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.minY)) - path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi * 1.5, - endAngle: 0.0, - clockwise: true) - } - // bottom-right - do { - path.addLine(to: .init(x: popoverRect.maxX, y: popoverRect.maxY - cornerRadius)) - path.addArc(withCenter: .init(x: popoverRect.maxX - cornerRadius, y: popoverRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: 0.0, - endAngle: .pi * 0.5, - clockwise: true) - } - // bottom-left - do { - path.addLine(to: .init(x: contentRect.minX + cornerRadius, y: contentRect.maxY)) - path.addArc(withCenter: .init(x: contentRect.minX + cornerRadius, y: contentRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: .pi * 0.5, - endAngle: .pi, - clockwise: true) - } - // arrow - if context.showsArrow { - let arrowTopPoint = CGPoint(x: contentRect.minX, y: arrowPoint.y - arrowSize.width / 2).inner.flattedValue - let arrowBottomPoint = CGPoint(x: contentRect.minX, y: arrowPoint.y + arrowSize.width / 2).inner.flattedValue - path.addLine(to: arrowBottomPoint) - do { - let controlPoint1 = CGPoint(x: arrowBottomPoint.x, y: arrowBottomPoint.y - arrowSize.width / 4).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowPoint.x, y: arrowPoint.y + arrowSize.width / 6).inner.flattedValue - path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) - } - do { - let controlPoint1 = CGPoint(x: arrowPoint.x, y: arrowPoint.y - arrowSize.width / 6).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowTopPoint.x, y: arrowTopPoint.y + arrowSize.width / 4).inner.flattedValue - path.addCurve(to: arrowTopPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) - } - } - // close - path.close() - - return path - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawRight.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawRight.swift deleted file mode 100644 index a34a864..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawRight.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// FSPopoverDrawRight.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/10. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -struct FSPopoverDrawRight: FSPopoverDrawable { - - func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { - - let popoverRect = CGRect(origin: offset, size: context.popoverSize) - let contentRect = CGRect(origin: offset, size: context.contentSize) - let arrowSize = context.arrowSize.inner.flattedValue - let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue - let cornerRadius = context.cornerRadius.inner.flattedValue - - let path = UIBezierPath() - // top-left - do { - path.move(to: .init(x: popoverRect.minX, y: popoverRect.minY + cornerRadius)) - path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi, - endAngle: .pi * 1.5, - clockwise: true) - } - // top-right - do { - path.addLine(to: .init(x: contentRect.maxX - cornerRadius, y: contentRect.minY)) - path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: contentRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi * 1.5, - endAngle: 0.0, - clockwise: true) - } - // arrow - if context.showsArrow { - let arrowTopPoint = CGPoint(x: contentRect.maxX, y: arrowPoint.y - arrowSize.width / 2).inner.flattedValue - let arrowBottomPoint = CGPoint(x: contentRect.maxX, y: arrowPoint.y + arrowSize.width / 2).inner.flattedValue - path.addLine(to: arrowTopPoint) - do { - let controlPoint1 = CGPoint(x: arrowTopPoint.x, y: arrowTopPoint.y + arrowSize.width / 4).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowPoint.x, y: arrowPoint.y - arrowSize.width / 6).inner.flattedValue - path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) - } - do { - let controlPoint1 = CGPoint(x: arrowPoint.x, y: arrowPoint.y + arrowSize.width / 6).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowBottomPoint.x, y: arrowBottomPoint.y - arrowSize.width / 4).inner.flattedValue - path.addCurve(to: arrowBottomPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) - } - } - // bottom-right - do { - path.addLine(to: .init(x: contentRect.maxX, y: contentRect.maxY - cornerRadius)) - path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: contentRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: 0.0, - endAngle: .pi * 0.5, - clockwise: true) - } - // bottom-left - do { - path.addLine(to: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY)) - path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: .pi * 0.5, - endAngle: .pi, - clockwise: true) - } - // close - path.close() - - return path - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawUp.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawUp.swift deleted file mode 100644 index 6fc5174..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawUp.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// FSPopoverDrawUp.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/10. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -struct FSPopoverDrawUp: FSPopoverDrawable { - - func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath { - - let popoverRect = CGRect(origin: offset, size: context.popoverSize) - let contentRect = CGRect(origin: .init(x: popoverRect.minX, y: popoverRect.minY + context.arrowSize.height), - size: context.contentSize) - let arrowSize = context.arrowSize.inner.flattedValue - let arrowPoint = context.arrowPoint.inner.offset(offset).inner.flattedValue - let cornerRadius = context.cornerRadius.inner.flattedValue - - let path = UIBezierPath() - // top-left - do { - path.move(to: .init(x: contentRect.minX, y: contentRect.minY + cornerRadius)) - path.addArc(withCenter: .init(x: contentRect.minX + cornerRadius, y: contentRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi, - endAngle: .pi * 1.5, - clockwise: true) - } - // arrow - if context.showsArrow { - path.addLine(to: .init(x: arrowPoint.x - arrowSize.width / 2, y: contentRect.minY)) - do { - let controlPoint1 = CGPoint(x: arrowPoint.x - arrowSize.width / 4, y: contentRect.minY).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowPoint.x - arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue - path.addCurve(to: arrowPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) - } - do { - let controlPoint1 = CGPoint(x: arrowPoint.x + arrowSize.width / 6, y: arrowPoint.y).inner.flattedValue - let controlPoint2 = CGPoint(x: arrowPoint.x + arrowSize.width / 4, y: contentRect.minY).inner.flattedValue - path.addCurve(to: .init(x: arrowPoint.x + arrowSize.width / 2, y: contentRect.minY), - controlPoint1: controlPoint1, - controlPoint2: controlPoint2) - } - } - // top-right - do { - path.addLine(to: .init(x: popoverRect.maxX - cornerRadius, y: contentRect.minY)) - path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: contentRect.minY + cornerRadius), - radius: cornerRadius, - startAngle: .pi * 1.5, - endAngle: 0.0, - clockwise: true) - } - // bottom-right - do { - path.addLine(to: .init(x: contentRect.maxX, y: popoverRect.maxY - cornerRadius)) - path.addArc(withCenter: .init(x: contentRect.maxX - cornerRadius, y: popoverRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: 0.0, - endAngle: .pi * 0.5, - clockwise: true) - } - // bottom-left - do { - path.addLine(to: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY)) - path.addArc(withCenter: .init(x: popoverRect.minX + cornerRadius, y: popoverRect.maxY - cornerRadius), - radius: cornerRadius, - startAngle: .pi * 0.5, - endAngle: .pi, - clockwise: true) - } - // close - path.close() - - return path - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawable.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawable.swift deleted file mode 100644 index 1933f3d..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawable.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// FSPopoverDrawable.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/12. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -protocol FSPopoverDrawable { - func generatePath(with context: FSPopoverDrawContext, offset: CGPoint) -> UIBezierPath -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawer.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawer.swift deleted file mode 100644 index 4d83034..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Draw/FSPopoverDrawer.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// FSPopoverDrawer.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/10. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -struct FSPopoverDrawer { - - // MARK: Properties/Internal - - let context: FSPopoverDrawContext - - // MARK: Properties/Private - - private let drawable: FSPopoverDrawable - - // MARK: Initialization - - init(context: FSPopoverDrawContext) { - self.context = context - switch context.arrowDirection { - case .up: - drawable = FSPopoverDrawUp() - case .down: - drawable = FSPopoverDrawDown() - case .left: - drawable = FSPopoverDrawLeft() - case .right: - drawable = FSPopoverDrawRight() - } - } - - // MARK: Internal - - func generatePath() -> UIBezierPath { - return drawable.generatePath(with: context, offset: .zero) - } - - func generateBorderImage() -> UIImage? { - - guard - context.borderWidth > 0.0, - let borderColor = context.borderColor - else { - return nil - } - - // The border is clipped in half after drawing, so make it double here. - let borderWidth = context.borderWidth * 2 - let size = CGSize(width: context.popoverSize.width + borderWidth * 2, - height: context.popoverSize.height + borderWidth * 2) - - // border image - let borderImage: UIImage? = { - - UIGraphicsBeginImageContextWithOptions(size, false, 0) - - let path = drawable.generatePath(with: context, offset: .init(x: borderWidth, y: borderWidth)) - path.lineWidth = borderWidth - borderColor.setStroke() - path.stroke() - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return image - }() - - // mask image - let maskImage: UIImage? = { - - UIGraphicsBeginImageContextWithOptions(size, false, 0) - - let path = drawable.generatePath(with: context, offset: .init(x: borderWidth, y: borderWidth)) - UIColor.white.setFill() - path.fill() - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return image - }() - - // create image mask - - guard - let cgimage = maskImage?.cgImage, - let dataProvider = cgimage.dataProvider - else { - return nil - } - - let width = cgimage.width - let height = cgimage.height - let bytesPerRow = cgimage.bytesPerRow - let bitsPerPixel = cgimage.bitsPerPixel - let bitsPerComponent = cgimage.bitsPerComponent - - guard - let mask = CGImage(maskWidth: width, - height: height, - bitsPerComponent: bitsPerComponent, - bitsPerPixel: bitsPerPixel, - bytesPerRow: bytesPerRow, - provider: dataProvider, - decode: nil, - shouldInterpolate: false), - let maskingCgImage = borderImage?.cgImage?.masking(mask) - else { - return nil - } - - return UIImage(cgImage: maskingCgImage, scale: UIScreen.main.scale, orientation: .up) - } - - func generateShadowImage() -> UIImage? { - - guard - context.shadowRadius > 0.0, - context.shadowOpacity > 0.0, - let shadowColor = context.shadowColor - else { - return nil - } - - // The shadow area is gradually reduced from the inside to the outside, - // and the visible part of the shadow is a bit larger than the set `shadowRadius`, - // so it is increased here to avoid clipping the shadow. - let radius = context.shadowRadius + context.borderWidth - let inset = radius + 10.0 - let size = CGSize(width: context.popoverSize.width + inset * 2, - height: context.popoverSize.height + inset * 2) - - // shadow image - let shadowImage: UIImage? = { - - UIGraphicsBeginImageContextWithOptions(size, false, 0) - - guard let context = UIGraphicsGetCurrentContext() else { - return nil - } - - let path = drawable.generatePath(with: self.context, offset: .init(x: inset, y: inset)) - - let layer = CAShapeLayer() - layer.path = path.cgPath - layer.fillColor = UIColor.white.cgColor - layer.shadowColor = shadowColor.cgColor - layer.shadowRadius = radius - layer.shadowOffset = .zero - layer.shadowOpacity = min(1.0, self.context.shadowOpacity) - layer.magnificationFilter = .nearest - layer.render(in: context) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return image - }() - - // mask image - let maskImage: UIImage? = { - - UIGraphicsBeginImageContextWithOptions(size, false, 0) - - let path = drawable.generatePath(with: context, offset: .init(x: inset, y: inset)) - UIColor.white.setFill() - path.fill() - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return image - }() - - // create image mask - - guard - let cgimage = maskImage?.cgImage, - let dataProvider = cgimage.dataProvider - else { - return nil - } - - let width = cgimage.width - let height = cgimage.height - let bytesPerRow = cgimage.bytesPerRow - let bitsPerPixel = cgimage.bitsPerPixel - let bitsPerComponent = cgimage.bitsPerComponent - - guard - let mask = CGImage(maskWidth: width, - height: height, - bitsPerComponent: bitsPerComponent, - bitsPerPixel: bitsPerPixel, - bytesPerRow: bytesPerRow, - provider: dataProvider, - decode: nil, - shouldInterpolate: false), - let maskingCgImage = shadowImage?.cgImage?.masking(mask) - else { - return nil - } - - let layer = CALayer() - layer.bounds = .init(origin: .zero, size: size) - layer.contents = maskingCgImage - layer.contentsScale = UIScreen.main.scale - layer.magnificationFilter = .nearest - let renderer = UIGraphicsImageRenderer(size: size) - return renderer.image { context in - layer.render(in: context.cgContext) - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/CoreGraphicsExtensions.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/CoreGraphicsExtensions.swift deleted file mode 100644 index c8c41ab..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/CoreGraphicsExtensions.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// CoreGraphicsExtensions.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/9. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -private func _Flat(_ x: T) -> T { - guard - x != T.leastNormalMagnitude, - x != T.leastNonzeroMagnitude, - x != T.greatestFiniteMagnitude - else { - return x - } - let scale: T = T(Int(UIScreen.main.scale)) - let flattedValue = ceil(x * scale) / scale - return flattedValue -} - -extension FSPopoverViewInternalWrapper where Base == CGRect { - - var flattedValue: CGRect { - return .init(origin: base.origin.inner.flattedValue, - size: base.size.inner.flattedValue) - } -} - -extension FSPopoverViewInternalWrapper where Base == CGSize { - - var flattedValue: CGSize { - return .init(width: base.width.inner.flattedValue, - height: base.height.inner.flattedValue) - } -} - -extension FSPopoverViewInternalWrapper where Base == CGPoint { - - var flattedValue: CGPoint { - return .init(x: base.x.inner.flattedValue, - y: base.y.inner.flattedValue) - } - - func offset(_ offset: CGPoint) -> CGPoint { - return .init(x: base.x + offset.x, y: base.y + offset.y) - } -} - -extension FSPopoverViewInternalWrapper where Base == CGFloat { - - var flattedValue: CGFloat { - return _Flat(base) - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/NSAttributedString+FSPopoverView.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/NSAttributedString+FSPopoverView.swift deleted file mode 100644 index c72db51..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/NSAttributedString+FSPopoverView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// NSAttributedString+FSPopoverView.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/24. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit -import Foundation - -extension FSPopoverViewInternalWrapper where Base: NSAttributedString { - - static func size(of attributedString: NSAttributedString?, limitedSize: CGSize? = .zero) -> CGSize { - guard let att_string = attributedString, !att_string.string.isEmpty else { - return .zero - } - let constraints: CGSize = { - if let size = limitedSize, size.width > 0, size.height > 0 { - return .init(width: ceil(size.width), height: ceil(size.height)) - } - return CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) - }() - let size = att_string.boundingRect(with: constraints, - options: [.usesLineFragmentOrigin, .usesFontLeading], - context: nil).size - return .init(width: ceil(size.width), height: ceil(size.height)) - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/UIColor+FSPopoverView.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/UIColor+FSPopoverView.swift deleted file mode 100644 index 3c8d792..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Extensions/UIColor+FSPopoverView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// UIColor+FSPopoverView.swift -// FSPopoverView -// -// Created by Sheng on 2023/12/2. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -extension FSPopoverViewInternalWrapper where Base: UIColor { - - static func color(hexed hex: String) -> UIColor? { - - guard hex.count > 0 else { - return nil - } - - var colorString = hex.uppercased() - do { - // remove prefix `#` / `0x`。 - if colorString.hasPrefix("#") { - colorString.removeFirst() - } else if colorString.hasPrefix("0x") { - colorString.removeFirst(2) - } else if colorString.hasPrefix("0X") { - colorString.removeFirst(2) - } - } - - if colorString.isEmpty { - return nil - } - - func _colorComponent(from string: String, location: Int, length: Int) -> CGFloat { - let startIndex = string.index(string.startIndex, offsetBy: location) - let endIndex = string.index(startIndex, offsetBy: length) - let substring = String(string[startIndex.. { - let base: Base - fileprivate init(_ base: Base) { - self.base = base - } -} - -protocol FSPopoverViewInternalCompatible: AnyObject {} -extension FSPopoverViewInternalCompatible { - static var inner: FSPopoverViewInternalWrapper.Type { - get { return FSPopoverViewInternalWrapper.self } - set {} - } - var inner: FSPopoverViewInternalWrapper { - get { return FSPopoverViewInternalWrapper(self) } - set {} - } -} - -protocol FSPopoverViewInternalCompatibleValue {} -extension FSPopoverViewInternalCompatibleValue { - static var inner: FSPopoverViewInternalWrapper.Type { - get { return FSPopoverViewInternalWrapper.self } - set {} - } - var inner: FSPopoverViewInternalWrapper { - get { return FSPopoverViewInternalWrapper(self) } - set {} - } -} - -extension UIView: FSPopoverViewInternalCompatible {} -extension UIColor: FSPopoverViewInternalCompatible {} -extension NSAttributedString: FSPopoverViewInternalCompatible {} - -extension CGRect: FSPopoverViewInternalCompatibleValue {} -extension CGSize: FSPopoverViewInternalCompatibleValue {} -extension CGPoint: FSPopoverViewInternalCompatibleValue {} -extension CGFloat: FSPopoverViewInternalCompatibleValue {} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Tools/FSPopoverViewDelegateRouter.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Tools/FSPopoverViewDelegateRouter.swift deleted file mode 100644 index af87669..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Tools/FSPopoverViewDelegateRouter.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// FSPopoverViewDelegateRouter.swift -// FSPopoverView -// -// Created by Sheng on 2022/4/13. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -final class FSPopoverViewDelegateRouter: NSObject { - - var gestureRecognizerShouldBeginHandler: ((_ gestureRecognizer: UIGestureRecognizer) -> Bool)? -} - -extension FSPopoverViewDelegateRouter: UIGestureRecognizerDelegate { - - func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if let handler = gestureRecognizerShouldBeginHandler { - return handler(gestureRecognizer) - } - return false - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Internal/Views/_FSSeparatorView.swift b/crush/Crush/Src/Components/UI/Popover/Internal/Views/_FSSeparatorView.swift deleted file mode 100644 index 731d317..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Internal/Views/_FSSeparatorView.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// _FSSeparatorView.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/24. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -final class _FSSeparatorView: UIView { - - // MARK: Properties/Internal - - var color: UIColor? { - didSet { - colorLayer.backgroundColor = color?.cgColor - } - } - - // MARK: Properties/Override - - @available(*, unavailable) - override var backgroundColor: UIColor? { - get { return nil } - set { super.backgroundColor = nil } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if #unavailable(iOS 17) { - colorLayer.backgroundColor = color?.cgColor - } - } - - // MARK: Properties/Private - - private let colorLayer = CAShapeLayer() - - // MARK: Initialization - - override public init(frame: CGRect) { - super.init(frame: frame) - p_didInitialize() - } - - required public init?(coder: NSCoder) { - super.init(coder: coder) - p_didInitialize() - } - - // MARK: Override - - override func layoutSubviews() { - super.layoutSubviews() - colorLayer.frame = .init(origin: .zero, size: frame.size) - } - - // MARK: Private - - private func p_didInitialize() { - color = UIColor(red: 0.90, green: 0.90, blue: 0.90, alpha: 1.00) - layer.addSublayer(colorLayer) - if #available(iOS 17, *) { - registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (self: Self, previousTraitCollection: UITraitCollection) in - self.colorLayer.backgroundColor = self.color?.cgColor - } - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/List/Cell/FSPopoverListCell.swift b/crush/Crush/Src/Components/UI/Popover/List/Cell/FSPopoverListCell.swift deleted file mode 100644 index 17f2a52..0000000 --- a/crush/Crush/Src/Components/UI/Popover/List/Cell/FSPopoverListCell.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// FSPopoverListCell.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/23. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -/// Abstract base class, cannot be used directly, must be inherited for use. -open class FSPopoverListCell: UIView { - - // MARK: Properties/Open - - open var isHighlighted = false { - didSet { - highlightedView.isHidden = !isHighlighted - } - } - - // MARK: Properties/Public - - public let item: FSPopoverListItem - - // MARK: Properties/Private - - private let separatorView = _FSSeparatorView() - - private let highlightedView = UIView() - - private var separatorConstraints = [NSLayoutConstraint]() - - // MARK: Initialization - - public required init(item: FSPopoverListItem) { - #if DEBUG - let abstractName = String(describing: FSPopoverListCell.self) - if "\(type(of: self))" == abstractName { - fatalError("\(abstractName) is abstract base class, cannot be used directly, must be inherited for use.") - } - #endif - self.item = item - super.init(frame: .zero) - p_didInitialize() - } - - @available(*, unavailable) - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Override - - open override func layoutSubviews() { - super.layoutSubviews() - sendSubviewToBack(highlightedView) - bringSubviewToFront(separatorView) - } - - // MARK: Private - - private func p_didInitialize() { - defer { - didInitialize() - } - backgroundColor = .clear - separatorView.isHidden = true - highlightedView.isHidden = true - separatorView.translatesAutoresizingMaskIntoConstraints = false - highlightedView.translatesAutoresizingMaskIntoConstraints = false - addSubview(separatorView) - addSubview(highlightedView) - do { - addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|", - metrics: nil, - views: ["view": highlightedView])) - addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", - metrics: nil, - views: ["view": highlightedView])) - } - } - - // MARK: Open - - /// Invoked after initialization. - /// Subclass can do initialization work in this method. - open func didInitialize() {} - - /// update contents. - /// - /// - Subclass can update contents in this method. - /// - This method will be called when the item that binding - /// with current view calls method `reload(_:)`. - /// - ⚠️ Subclasses **must** call `super.renderContents()` when - /// overriding this method, otherwise some bugs may occur. - /// - open func renderContents() { - defer { p_remakeConstraints() } - separatorView.color = item.separatorColor - separatorView.isHidden = item.isSeparatorHidden - highlightedView.backgroundColor = item.highlightedColor - } -} - -// MARK: - Private - -private extension FSPopoverListCell { - - func p_remakeConstraints() { - NSLayoutConstraint.deactivate(separatorConstraints) - separatorConstraints.removeAll() - switch item.scrollDirection { - case .vertical: - let hConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-left-[view]-right-|", - metrics: [ - "left": item.separatorInset.left, - "right": item.separatorInset.right - ], - views: ["view": separatorView]) - let vConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[view(==height)]|", - metrics: ["height": 1.0 / UIScreen.main.scale], - views: ["view": separatorView]) - separatorConstraints += hConstraints + vConstraints - case .horizontal: - let hConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[view(==width)]|", - metrics: ["width": 1.0 / UIScreen.main.scale], - views: ["view": separatorView]) - let vConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-top-[view]-bottom-|", - metrics: [ - "top": item.separatorInset.top, - "bottom": item.separatorInset.bottom - ], - views: ["view": separatorView]) - separatorConstraints += hConstraints + vConstraints - } - addConstraints(separatorConstraints) - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/List/FSPopoverListView.swift b/crush/Crush/Src/Components/UI/Popover/List/FSPopoverListView.swift deleted file mode 100644 index d62e6ba..0000000 --- a/crush/Crush/Src/Components/UI/Popover/List/FSPopoverListView.swift +++ /dev/null @@ -1,345 +0,0 @@ -// -// FSPopoverListView.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/20. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -open class FSPopoverListView: FSPopoverView, FSPopoverViewDataSource { - - // MARK: ScrollDirection - - public enum ScrollDirection { - case vertical - case horizontal - } - - // MARK: Properties/Open - - /// The scroll direction of the list. - open var scrollDirection: FSPopoverListView.ScrollDirection { - didSet { - if scrollDirection != oldValue { - setNeedsReload() - } - } - } - - /// The list items. - /// An item represents a cell. - open var items: [FSPopoverListItem]? { - didSet { - setNeedsReload() - } - } - - /// Whether needs to dismiss list view when user tap the blank area. - /// Defaults to true. - open var shouldDismissOnTapOutside = true - - // MARK: Properties/Public - - /// Limits the number of visible items displayed in the list view. - /// It means there is no limit if this value less than or equal to 0. - /// Defaults to 0. - public final var maximumCountOfVisibleItems = 0 { - didSet { - if maximumCountOfVisibleItems != oldValue { - setNeedsReload() - } - } - } - - /// Auto dismiss the list view when an item is selected. - /// Defaults to true. - public final var dismissWhenSelected = true - - /// The background view's background color. - /// Defaults to white. - public final var backgroundColor: UIColor? { - get { return backgroundView.backgroundColor } - set { backgroundView.backgroundColor = newValue } - } - - // MARK: Properties/Private - - private let scrollView = _ListScrollView() - - private let backgroundView = UIView() - - private var contentSize = CGSize.zero - - private var scrollContentSize = CGSize.zero - - // MARK: Initialization - - public init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { - self.scrollDirection = scrollDirection - super.init() - p_didInitialize() - } - - // MARK: Override - - open override func reloadData() { - - super.reloadData() - - scrollView.contentSize = scrollContentSize - - let items = items ?? [] - - scrollView.cells.forEach { $0.removeFromSuperview() } - scrollView.cells.removeAll() - - var lastCell: FSPopoverListCell? - items.forEach { item in - let cell = item.cellType.init(item: item) - do { - var frame = lastCell?.frame ?? .zero - switch scrollDirection { - case .vertical: - frame.origin.y = frame.maxY - frame.size.width = contentSize.width - frame.size.height = item.size.height - case .horizontal: - frame.origin.x = frame.maxX - frame.size.width = item.size.width - frame.size.height = contentSize.height - } - cell.frame = frame - } - cell.renderContents() - scrollView.addSubview(cell) - scrollView.cells.append(cell) - lastCell = cell - item.reloadHandler = { [unowned cell, unowned self] _, type in - if type == .reload { - self.setNeedsReload() - } else { - cell.renderContents() - } - } - } - } - - // MARK: Open/FSPopoverViewDataSource - - open func backgroundView(for popoverView: FSPopoverView) -> UIView? { - return backgroundView - } - - open func contentView(for popoverView: FSPopoverView) -> UIView? { - return scrollView - } - - open func contentSize(for popoverView: FSPopoverView) -> CGSize { - guard - let upMaxSize = maximumContentSizeOf(direction: .up), - let downMaxSize = maximumContentSizeOf(direction: .down), - let leftMaxSize = maximumContentSizeOf(direction: .left), - let rightMaxSize = maximumContentSizeOf(direction: .right) - else { - return .zero - } - if let items = items { - var contentSize = CGSize.zero - do { - // original content size. - switch scrollDirection { - case .vertical: - contentSize.width = items.map { $0.size.width }.max() ?? 0 - contentSize.height = items.map { $0.size.height }.reduce(0, +) - case .horizontal: - contentSize.width = items.map { $0.size.width }.reduce(0, +) - contentSize.height = items.map { $0.size.height }.max() ?? 0 - } - scrollContentSize = contentSize - } - do { - // limits maximum content size. - let maxSize: CGSize - if autosetsArrowDirection { - // priority: up > down > left > right - if upMaxSize.width >= contentSize.width && upMaxSize.height >= contentSize.height { - maxSize = upMaxSize - } else if downMaxSize.width >= contentSize.width && downMaxSize.height >= contentSize.height { - maxSize = downMaxSize - } else if leftMaxSize.width >= contentSize.width && leftMaxSize.height >= contentSize.height { - maxSize = leftMaxSize - } else if rightMaxSize.width >= contentSize.width && rightMaxSize.height >= contentSize.height { - maxSize = rightMaxSize - } else { - // Use up maximum size if there is no compatible size. - maxSize = upMaxSize - } - } else { - switch arrowDirection { - case .up: - maxSize = upMaxSize - case .down: - maxSize = downMaxSize - case .left: - maxSize = leftMaxSize - case .right: - maxSize = rightMaxSize - } - } - contentSize.width = min(contentSize.width, maxSize.width) - contentSize.height = min(contentSize.height, maxSize.height) - } - do { - // limits maximum count of visible items. - if maximumCountOfVisibleItems > 0, maximumCountOfVisibleItems < items.count { - let visibleItems = items[0.. UIEdgeInsets { - var insets = containerView?.safeAreaInsets ?? .zero - if insets.top <= 0 { insets.top = 10.0 } - if insets.bottom <= 0 { insets.bottom = 10.0 } - if insets.left <= 0 { insets.left = 10.0 } - if insets.right <= 0 { insets.right = 10.0 } - return insets - } - - open func popoverViewShouldDismissOnTapOutside(_ popoverView: FSPopoverView) -> Bool { - return shouldDismissOnTapOutside - } - - // MARK: Open - - open func didSelectItem(_ item: FSPopoverListItem) { - let operation: () -> Void = { - item.selectedHandler?(item) - } - if dismissWhenSelected { - dismiss(animated: true, isSelection: true) { - operation() - } - } else { - operation() - } - } -} - -// MARK: - Private - -private extension FSPopoverListView { - - /// Invoked after initialization. - func p_didInitialize() { - dataSource = self - backgroundView.backgroundColor = FSPopoverView.fs_appearance().backgroundColor - scrollView.selectedCellHandler = { [unowned self] cell in - self.didSelectItem(cell.item) - } - } -} - - -// MARK: - _ListScrollView - -private class _ListScrollView: UIScrollView { - - // MARK: Properties/Fileprivate - - var cells = [FSPopoverListCell]() - - var selectedCellHandler: ((_ cell: FSPopoverListCell) -> Void)? - - // MARK: Properties/Private - - private weak var selectedCell: FSPopoverListCell? - private weak var highlightedCell: FSPopoverListCell? - - // MARK: Override - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - highlightedCell?.isHighlighted = false - highlightedCell = nil - guard let touch = touches.first else { return } - let location = touch.location(in: self) - if let cell = p_cell(at: location), cell.item.isEnabled { - selectedCell = cell - if cell.item.selectionStyle != .none { - cell.isHighlighted = true - highlightedCell = cell - } - } - } - - override func touchesMoved(_ touches: Set, with event: UIEvent?) { - super.touchesEnded(touches, with: event) - guard let touch = touches.first else { return } - let location = touch.location(in: self) - if let cell = highlightedCell { - if cell.frame.contains(location) { - if !cell.isHighlighted { - cell.isHighlighted = true - } - } else { - if cell.isHighlighted { - cell.isHighlighted = false - } - } - } - } - - override func touchesEnded(_ touches: Set, with event: UIEvent?) { - super.touchesEnded(touches, with: event) - guard let touch = touches.first else { return } - let location = touch.location(in: self) - if let cell = p_cell(at: location) { - if let selectedCell = selectedCell, selectedCell === cell { - selectedCellHandler?(selectedCell) - } - } - selectedCell = nil - highlightedCell = nil - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - super.touchesCancelled(touches, with: event) - selectedCell = nil - highlightedCell?.isHighlighted = false - highlightedCell = nil - } - - // MARK: Private - - private func p_cell(at location: CGPoint) -> FSPopoverListCell? { - guard !cells.isEmpty else { return nil } - var result: FSPopoverListCell? = nil - for cell in cells { - if cell.frame.contains(location) { - result = cell - break - } - } - return result - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/List/Item/CLPopoverListTextItem.swift b/crush/Crush/Src/Components/UI/Popover/List/Item/CLPopoverListTextItem.swift deleted file mode 100644 index 42fc76c..0000000 --- a/crush/Crush/Src/Components/UI/Popover/List/Item/CLPopoverListTextItem.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CLPopoverListTextItem.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -class CLPopoverListTextItem: FSPopoverListTextItem{ - - override init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { - super.init(scrollDirection: scrollDirection) - - setupCLStyle() - } - - func setupCLStyle(){ - isSeparatorHidden = false - contentInset = .init(top: 16, left: 16.0, bottom: 16.0, right: 56.0) // right: 56 - // updateLayout() - isSeparatorHidden = true - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/List/Item/Defaults/FSPopoverListTextItem.swift b/crush/Crush/Src/Components/UI/Popover/List/Item/Defaults/FSPopoverListTextItem.swift deleted file mode 100644 index 68a551d..0000000 --- a/crush/Crush/Src/Components/UI/Popover/List/Item/Defaults/FSPopoverListTextItem.swift +++ /dev/null @@ -1,267 +0,0 @@ -// -// FSPopoverListTextItem.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/23. -// Copyright © 2023 Sheng. All rights reserved. -// -// FSPopoverListTextItem looks like: -// vertical: contents center in vertical -// NOTE: This mode is compatible with Right-to-Left language. -// ┌────────────────────────────────┬──────────────────────────────────────┐ -// │ │ -// │ │ │ -// │--[icon]--[title]--│ -// │ │ │ -// │ │ -// └────────────────────────────────┴──────────────────────────────────────┘ -// horizontal: contents center in horizontal -// ┌─────────────────────────┬──────────────────────────┐ -// │ │ -// │ │ │ -// │ [icon] │ -// │ │ │ -// │ │ -// │ │ │ -// ├ -[title]- ┤ -// │ │ │ -// │ │ -// └─────────────────────────┴──────────────────────────┘ -// - -import UIKit - -// final -public class FSPopoverListTextItem: FSPopoverListItem { - - // MARK: Properties/Public - - public var contentInset: UIEdgeInsets - - public var image: UIImage? - - public var title: String? - - /// The space between image and title. - /// This value will not work if one of image and title is nil. - /// Default value see `FSPopoverViewAppearance`. - public var spacing: CGFloat = FSPopoverViewAppearance.shared.spacing - - /// If nil, use a default font. - /// Default value see `FSPopoverViewAppearance`. - public var titleFont: UIFont? = FSPopoverViewAppearance.shared.textFont - - /// If nil, use a default color. - /// Default value see `FSPopoverViewAppearance`. - public var titleColor: UIColor? = FSPopoverViewAppearance.shared.textColor - - public var trailImage: UIImage? - - // MARK: Properties/Override - - public override var cellType: FSPopoverListCell.Type { - return FSPopoverListTextCell.self - } - - // MARK: Initialization - - public override init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { - switch scrollDirection { - case .vertical: - contentInset = .init(top: 8.0, left: 15.0, bottom: 8.0, right: 15.0) - case .horizontal: - contentInset = .init(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0) - } - super.init(scrollDirection: scrollDirection) - } - - // MARK: Public - - /// You need to call this method if you change any contents. - public func updateLayout() { - titleFont = titleFont ?? FSPopoverViewAppearance.shared.textFont - titleColor = titleColor ?? FSPopoverViewAppearance.shared.textColor - switch scrollDirection { - case .vertical: - p_updateLayoutForVertical() - case .horizontal: - p_updateLayoutForHorizontal() - } - } - - // MARK: Private - - private func p_updateLayoutForVertical() { - var size = CGSize.zero - let imageSize = image?.size - let titleSize = p_titleSize() - size.height = max(imageSize?.height ?? 0, titleSize?.height ?? 0) - size.height += contentInset.top + contentInset.bottom - size.width += contentInset.left - if let width = imageSize?.width { - size.width += width + spacing - } - if let width = titleSize?.width { - size.width += width - } - size.width += contentInset.right - self.size = size - } - - private func p_updateLayoutForHorizontal() { - var size = CGSize.zero - let imageSize = image?.size - let titleSize = p_titleSize() - size.width = max(imageSize?.width ?? 0, titleSize?.width ?? 0) - size.width += contentInset.left + contentInset.right - size.height += contentInset.top - if let height = imageSize?.height { - size.height += height + spacing - } - if let height = titleSize?.height { - size.height += height - } - size.height += contentInset.bottom - self.size = size - } - - private func p_titleSize() -> CGSize? { - guard let title = title, !title.isEmpty else { - return nil - } - let titleFont = titleFont ?? FSPopoverViewAppearance.shared.textFont - let attributedTitle = NSAttributedString(string: title, attributes: [.font: titleFont]) - return NSAttributedString.inner.size(of: attributedTitle) - } -} - - -// MARK: - FSPopoverListTextCell - -private class FSPopoverListTextCell: FSPopoverListCell { - - // MARK: Properties/Private - - private let imageView = UIImageView() - private let titleLabel = UILabel() - private let trailIcon = UIImageView() - - private lazy var stackView: UIStackView = { - let stack = UIStackView() - stack.axis = .horizontal - stack.alignment = .center - stack.translatesAutoresizingMaskIntoConstraints = false - return stack - }() - - private var stackConstraints = [NSLayoutConstraint]() - - // MARK: Override - - override func didInitialize() { - addSubview(stackView) - - titleLabel.setContentHuggingPriority(UILayoutPriority(244), for: .horizontal) - trailIcon.setContentHuggingPriority(UILayoutPriority(255), for: .horizontal) - } - - override func renderContents() { - super.renderContents() - guard let item = item as? FSPopoverListTextItem else { - return - } - stackView.axis = item.scrollDirection == .vertical ? .horizontal : .vertical - stackView.alpha = item.isEnabled ? 1.0 : 0.5 - stackView.spacing = item.spacing - do { - let views = stackView.arrangedSubviews - views.forEach { $0.removeFromSuperview() } - } - if let image = item.image { - imageView.image = image - stackView.addArrangedSubview(imageView) - } - if let title = item.title, !title.isEmpty { - titleLabel.text = title - titleLabel.font = item.titleFont - titleLabel.textColor = item.titleColor - stackView.addArrangedSubview(titleLabel) - } - - trailIcon.image = item.trailImage//UIImage(named: "litte_status_successful") - addSubview(trailIcon) - trailIcon.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - - p_remakeConstraints() - } - - // MARK: Private - - private func p_remakeConstraints() { - guard let item = item as? FSPopoverListTextItem else { - return - } - NSLayoutConstraint.deactivate(stackConstraints) - stackConstraints.removeAll() - switch item.scrollDirection { - case .vertical: - let leading = NSLayoutConstraint(item: stackView, - attribute: .leading, - relatedBy: .equal, - toItem: self, - attribute: .leading, - multiplier: 1.0, - constant: item.contentInset.left) - let trailing = NSLayoutConstraint(item: stackView, - attribute: .trailing, - relatedBy: .lessThanOrEqual, - toItem: self, - attribute: .trailing, - multiplier: 1.0, - constant: -item.contentInset.right) - let centerY = NSLayoutConstraint(item: stackView, - attribute: .centerY, - relatedBy: .equal, - toItem: self, - attribute: .centerY, - multiplier: 1.0, - constant: 0.0) - stackConstraints = [leading, trailing, centerY] - case .horizontal: - let top = NSLayoutConstraint(item: stackView, - attribute: .top, - relatedBy: .lessThanOrEqual, - toItem: self, - attribute: .top, - multiplier: 1.0, - constant: item.contentInset.top) - let centerX = NSLayoutConstraint(item: stackView, - attribute: .centerX, - relatedBy: .equal, - toItem: self, - attribute: .centerX, - multiplier: 1.0, - constant: 0.0) - let centerY = NSLayoutConstraint(item: stackView, - attribute: .centerY, - relatedBy: .lessThanOrEqual, - toItem: self, - attribute: .centerY, - multiplier: 1.0, - constant: 0.0) - let width = NSLayoutConstraint(item: stackView, - attribute: .width, - relatedBy: .lessThanOrEqual, - toItem: self, - attribute: .width, - multiplier: 1.0, - constant: 0.0) - centerY.priority = .defaultHigh - stackConstraints = [top, centerX, centerY, width] - } - addConstraints(stackConstraints) - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/List/Item/FSPopoverListItem.swift b/crush/Crush/Src/Components/UI/Popover/List/Item/FSPopoverListItem.swift deleted file mode 100644 index 5ab6427..0000000 --- a/crush/Crush/Src/Components/UI/Popover/List/Item/FSPopoverListItem.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// FSPopoverListItem.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/23. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -/// Abstract base class, cannot be used directly, must be inherited for use. -open class FSPopoverListItem { - - public enum ReloadType { - /// Only update contents of the cell binding with the - /// current item, no update in size and other cells. - case rerender - /// Reload the whole contents of FSPopoverListView. - case reload - } - - public enum SelectionStyle { - /// The cell has no distinct style for when it's selected. - case none - /// The cell has a gray background when it's selected. - case gray - } - - // MARK: Properties/Open - - /// The size of item. - /// - /// - The cell's frame.size may not equal to this size. - /// - /// - When the scroll direction of FSPopoverListView is `vertical`, - /// the list view picks out the largest `size.width` from its items as its width. - /// - /// - When the scroll direction of FSPopoverListView is `horizontal`, - /// the list view picks out the largest `size.height` from its items as its height. - /// - open var size: CGSize = .zero - - /// Type of the cell that binding with the item. - /// - /// - This type must be the subclass of FSPopoverListCell. - /// - open var cellType: FSPopoverListCell.Type { - return FSPopoverListCell.self - } - - // MARK: Properties/Public - - public let scrollDirection: FSPopoverListView.ScrollDirection - - public final var selectionStyle: FSPopoverListItem.SelectionStyle = .gray - - /// A closure to execute when the user selects the item. - /// This closure has no return value and takes the selected item object as its only parameter. - public final var selectedHandler: ((_ item: FSPopoverListItem) -> Void)? - - /// Whether the item is enabled. Defaults to true. - public final var isEnabled = true - - /// Default value see `FSPopoverViewAppearance`. - public final var separatorInset: UIEdgeInsets = FSPopoverViewAppearance.shared.separatorInset - - /// The color of separator. - /// Default value see `FSPopoverViewAppearance`. - public final var separatorColor: UIColor? = FSPopoverViewAppearance.shared.separatorColor - - /// Whether needs hide separator, defaults to true. - /// - /// - The separator is on the bottom when scroll direction is vertical. - /// - The separator is on the right when scroll direction is horizontal. - /// - public final var isSeparatorHidden = true - - /// The color of separator. - /// Default value see `FSPopoverViewAppearance`. - public final var highlightedColor: UIColor? = FSPopoverViewAppearance.shared.highlightedColor - - // MARK: Properties/Internal - - /// Used for reload operation. - /// - /// - Warning: - /// * ⚠️ Leave this closure alone, you should never use it. - /// - final var reloadHandler: ((FSPopoverListItem, FSPopoverListItem.ReloadType) -> Void)? - - // MARK: Initialization - - public init(scrollDirection: FSPopoverListView.ScrollDirection = .vertical) { - #if DEBUG - let abstractName = String(describing: FSPopoverListItem.self) - if "\(type(of: self))" == abstractName { - fatalError("\(abstractName) is abstract base class, cannot be used directly, must be inherited for use.") - } - #endif - self.scrollDirection = scrollDirection - } - - // MARK: Public - - /// Requests a reload operation for current item. - /// This method will not work if the cell has not been added to list view. - public final func reload(_ type: FSPopoverListItem.ReloadType = .rerender) { - reloadHandler?(self, type) - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionFade.swift b/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionFade.swift deleted file mode 100644 index c898ef5..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionFade.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// FSPopoverViewTransitionFade.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/23. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -public final class FSPopoverViewTransitionFade: FSPopoverViewAnimatedTransitioning { - - public init() {} - - public func animateTransition(transitionContext context: FSPopoverViewTransitionContext) { - - let popoverView = context.popoverView - let dimBackgroundView = context.dimBackgroundView - - switch context.scene { - case .present: - popoverView.alpha = 0.0 - dimBackgroundView.alpha = 0.0 - UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { - popoverView.alpha = 1.0 - dimBackgroundView.alpha = 1.0 - } completion: { _ in - context.completeTransition() - } - case .dismiss(_): - UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { - popoverView.alpha = 0.0 - dimBackgroundView.alpha = 0.0 - } completion: { _ in - popoverView.alpha = 1.0 - dimBackgroundView.alpha = 1.0 - context.completeTransition() - } - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionScale.swift b/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionScale.swift deleted file mode 100644 index 97cd734..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionScale.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// FSPopoverViewTransitionScale.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/15. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -public final class FSPopoverViewTransitionScale: FSPopoverViewAnimatedTransitioning { - - public var usingSpring = true - - public init() {} - - public func animateTransition(transitionContext context: FSPopoverViewTransitionContext) { - - let popoverView = context.popoverView - let arrowPoint = popoverView.arrowPoint - let dimBackgroundView = context.dimBackgroundView - - // anchor point - do { - let frame = popoverView.frame - popoverView.layer.anchorPoint = { - var x: CGFloat = 0.0, y: CGFloat = 0.0 - let arrowPointInPopover = CGPoint(x: arrowPoint.x - frame.minX, y: arrowPoint.y - frame.minY) - switch popoverView.arrowDirection { - case .up: - x = arrowPointInPopover.x / frame.width - case .down: - x = arrowPointInPopover.x / frame.width - y = 1.0 - case .left: - y = arrowPointInPopover.y / frame.height - case .right: - x = 1.0 - y = arrowPointInPopover.y / frame.height - } - return .init(x: x, y: y) - }() - // Needs to reset frame again after updating `layer.anchorPoint`, - // or the popover view will be in the wrong place. - popoverView.frame = frame - } - - switch context.scene { - case .present: - dimBackgroundView.alpha = 0.0 - UIView.animate(withDuration: 0.18, delay: 0.0, options: .curveEaseOut) { - dimBackgroundView.alpha = 1.0 - } - - popoverView.transform = .init(scaleX: 0.01, y: 0.01) - let duration: TimeInterval = usingSpring ? 0.38 : 0.18 - let spring: CGFloat = usingSpring ? 0.68 : 1.0 - UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: spring, initialSpringVelocity: 0.5) { - popoverView.transform = .identity - } completion: { _ in - context.completeTransition() - } - case .dismiss(let isSelection): - UIView.animate(withDuration: isSelection ? 0.25 : 0.18, delay: 0.0, options: .curveEaseOut) { - popoverView.alpha = 0.0 - dimBackgroundView.alpha = 0.0 - if !isSelection { - popoverView.transform = .init(scaleX: 0.01, y: 0.01) - } - } completion: { _ in - popoverView.alpha = 1.0 - popoverView.transform = .identity - dimBackgroundView.alpha = 1.0 - context.completeTransition() - } - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionTranslate.swift b/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionTranslate.swift deleted file mode 100644 index 656e683..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Transition/Defaults/FSPopoverViewTransitionTranslate.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// FSPopoverViewTransitionTranslate.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/23. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -public final class FSPopoverViewTransitionTranslate: FSPopoverViewAnimatedTransitioning { - - public init() {} - - public func animateTransition(transitionContext context: FSPopoverViewTransitionContext) { - - guard let containerView = context.popoverView.containerView else { - context.completeTransition() - return - } - - let popoverView = context.popoverView - let dimBackgroundView = context.dimBackgroundView - - let popoverFrame = popoverView.frame - let containerFrame = containerView.frame - let popoverInitialFrame: CGRect = { - var frame = popoverFrame - switch popoverView.arrowDirection { - case .up: - frame.origin.y = containerFrame.maxY - case .down: - frame.origin.y = containerFrame.minY - frame.height - case .left: - frame.origin.x = containerFrame.maxX - case .right: - frame.origin.x = containerFrame.minX - frame.width - } - return frame - }() - - switch context.scene { - case .present: - popoverView.frame = popoverInitialFrame - dimBackgroundView.alpha = 0.0 - UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { - popoverView.frame = popoverFrame - dimBackgroundView.alpha = 1.0 - } completion: { _ in - context.completeTransition() - } - case .dismiss(let isSelection): - UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseOut) { - if isSelection { - popoverView.alpha = 0.0 - } else { - popoverView.frame = popoverInitialFrame - } - dimBackgroundView.alpha = 0.0 - } completion: { _ in - popoverView.alpha = 1.0 - dimBackgroundView.alpha = 1.0 - context.completeTransition() - } - } - } -} diff --git a/crush/Crush/Src/Components/UI/Popover/Transition/FSPopoverViewAnimatedTransitioning.swift b/crush/Crush/Src/Components/UI/Popover/Transition/FSPopoverViewAnimatedTransitioning.swift deleted file mode 100644 index f020cc0..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Transition/FSPopoverViewAnimatedTransitioning.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// FSPopoverViewAnimatedTransitioning.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/15. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -public protocol FSPopoverViewAnimatedTransitioning: AnyObject { - - /// This method will be called when presenting or dismissing a popover view. - /// Use this method to configure the animations associated with your custom transition. - /// - /// - Important: - /// * You must call the method `completeTransition()` of `context` when the transition is finished. - /// Otherwise the popover view will work unexpected. - /// - func animateTransition(transitionContext context: FSPopoverViewTransitionContext) -} diff --git a/crush/Crush/Src/Components/UI/Popover/Transition/FSPopoverViewTransitionContext.swift b/crush/Crush/Src/Components/UI/Popover/Transition/FSPopoverViewTransitionContext.swift deleted file mode 100644 index 5efd3b3..0000000 --- a/crush/Crush/Src/Components/UI/Popover/Transition/FSPopoverViewTransitionContext.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// FSPopoverViewTransitionContext.swift -// FSPopoverView -// -// Created by Sheng on 2023/11/23. -// Copyright © 2023 Sheng. All rights reserved. -// - -import UIKit - -public final class FSPopoverViewTransitionContext { - - // MARK: Sence - - public enum Scene { - - case present - - /// - Prameters: - /// - isSelection: This value is usually used in a list. - case dismiss(_ isSelection: Bool = false) - } - - // MARK: Properties/Public - - public let scene: FSPopoverViewTransitionContext.Scene - - public let popoverView: FSPopoverView - - public let dimBackgroundView: UIView - - // MARK: Properties/Internal - - var onDidCompleteTransition: (() -> Void)? - - // MARK: Initialization - - init(scene: FSPopoverViewTransitionContext.Scene, popoverView: FSPopoverView, dimBackgroundView: UIView) { - self.scene = scene - self.popoverView = popoverView - self.dimBackgroundView = dimBackgroundView - } - - // MARK: Public - - /// When the animation ends, you must call this method to - /// tell the popover view that the animation has ended. - public func completeTransition() { - onDidCompleteTransition?() - } -} diff --git a/crush/Crush/Src/Components/UI/Refresher/RefreshAnimator.swift b/crush/Crush/Src/Components/UI/Refresher/RefreshAnimator.swift deleted file mode 100755 index 7a094a3..0000000 --- a/crush/Crush/Src/Components/UI/Refresher/RefreshAnimator.swift +++ /dev/null @@ -1,344 +0,0 @@ -// -// RefreshAnimator.swift -// LegendTeam -// -// Created by lym on 2021/12/27. -// - -import Foundation -import Lottie -import MJRefresh -import UIKit - -@objcMembers class RefreshHeaderAnimator: MJRefreshHeader { - var offsetStatusBarHeight: Bool = false - - private lazy var animationView: LottieAnimationView = { - let animation = LottieAnimation.named("single_ring") - let animationView = LottieAnimationView(animation: animation) - - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - animationView.size = CGSize(width: 16, height: 16) - addSubview(animationView) - return animationView - }() - - private let kMaxHeaderPanHeight: CGFloat = 50.0 - - override func prepare() { - super.prepare() - animationView.isHidden = false - mj_h = kMaxHeaderPanHeight - mj_w = UIScreen.width - } - - override func placeSubviews() { - super.placeSubviews() - - if animationView.mj_x == 0 { - if offsetStatusBarHeight { - mj_h = kMaxHeaderPanHeight + UIWindow.statusBarHeight - } else { - mj_h = kMaxHeaderPanHeight - } - } - - animationView.center = CGPoint(x: mj_w / 2, y: mj_h / 2) - if offsetStatusBarHeight { - animationView.mj_y = 15 + UIWindow.statusBarHeight - } else { - animationView.mj_y = 15 - } - } - - override var state: MJRefreshState { - willSet { - guard state != newValue else { - return - } - } - didSet { - switch state { - case .willRefresh: - self.animationView.alpha = 1 - case .refreshing: - DispatchQueue.main.async { [weak self] in - self?.animationView.alpha = 1 - self?.animationView.play() - } - case .pulling: - let generator = UIImpactFeedbackGenerator(style: .light) - generator.prepare() - generator.impactOccurred() - case .idle: - DispatchQueue.main.async { [weak self] in - self?.animationView.alpha = 0.6 - self?.animationView.currentProgress = 0 - self?.animationView.stop() - } - - default: - if !(oldValue == .refreshing) { - animationView.stop() - } - } - } - } - - override func scrollViewContentOffsetDidChange(_ change: [AnyHashable: Any]?) { - super.scrollViewContentOffsetDidChange(change) - - if animationView.isAnimationPlaying { - return - } - - switch state { - case .idle, .pulling: - guard let dic = change else { - return - } - guard let value = dic[NSKeyValueChangeKey.newKey] as? NSValue else { - return - } - - let offsetY = value.cgPointValue.y - // 下拉才执行 - if offsetY > 0 { - return - } - //dlog("offsetY: \(offsetY)") - let newOffsetY = -offsetY - 25 - var progress = abs(newOffsetY) / kMaxHeaderPanHeight - progress = progress >= 1 ? 1 : progress - // * 一个系数,免得动画太快 - animationView.currentProgress = progress * 0.4 - animationView.alpha = progress + 0.5 - default: - break - } - } -} - -@objcMembers class RefreshFooterAnimator: MJRefreshAutoFooter { - var noMoreData: String = "" - - private lazy var animationView: LottieAnimationView = { - let animation = LottieAnimation.named("single_ring") - let animationView = LottieAnimationView(animation: animation) - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.size = CGSize(width: 16, height: 16) - animationView.backgroundColor = .clear - addSubview(animationView) - return animationView - }() - - private var noMoreDataCustomView = UIView() { - didSet { - stateLabel.isHidden = true - noMoreDataCustomView.isHidden = true - addSubview(noMoreDataCustomView) - sendSubviewToBack(noMoreDataCustomView) - } - } - - private lazy var stateLabel: UILabel = { - let label = UILabel() - label.textColor = .white - label.font = .systemFont(ofSize: 14)//.popRegular(size: 12) - label.isHidden = true - label.textAlignment = .center - label.text = " " - addSubview(label) - return label - }() - - private let kMaxFooterPanHeight: CGFloat = 50 - - override func prepare() { - super.prepare() - noMoreData = "No more data~"//R.string.localizable.no_more_data.localized()//"No more data~" - - mj_h = kMaxFooterPanHeight - } - - override func placeSubviews() { - super.placeSubviews() - - if animationView.mj_x == 0 { - animationView.center = CGPoint(x: mj_w / 2, y: mj_h / 2) - animationView.mj_y = 10 - } - - if stateLabel.mj_y == 0 { - stateLabel.mj_x = 0 - stateLabel.mj_y = 15 - stateLabel.mj_w = mj_w - stateLabel.mj_h = 20 - } - noMoreDataCustomView.frame = bounds - bringSubviewToFront(stateLabel) - } - - override var state: MJRefreshState { - willSet { - guard state != newValue else { - return - } - } - didSet { - noMoreDataCustomView.isHidden = true - stateLabel.isHidden = true - - switch state { - case .refreshing: - DispatchQueue.main.async { [weak self] in - self?.animationView.currentProgress = 0 - self?.animationView.isHidden = false - self?.animationView.alpha = 1 - self?.animationView.play() - } - - case .pulling: - let generator = UIImpactFeedbackGenerator(style: .light) - generator.prepare() - generator.impactOccurred() - - case .noMoreData: - stateLabel.isHidden = false - stateLabel.text = noMoreData - noMoreDataCustomView.isHidden = false - animationView.alpha = 0 - animationView.stop() - case .idle: - animationView.alpha = 0 - animationView.stop() - - default: - break - } - } - } - - override func scrollViewContentSizeDidChange(_ change: [AnyHashable: Any]?) { - super.scrollViewContentSizeDidChange(change) - - guard let scrollView = scrollView else { return } - - let contentHeight = scrollView.mj_contentH + ignoredScrollViewContentInsetBottom - var scrollHeight: CGFloat = 0 - - if #available(iOS 11.0, *) { - scrollHeight = scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom - - } else { - scrollHeight = scrollView.mj_h - scrollViewOriginalInset.top + ignoredScrollViewContentInsetBottom - } - mj_y = max(contentHeight, scrollHeight) - } - - override func scrollViewContentOffsetDidChange(_ change: [AnyHashable: Any]?) { - super.scrollViewContentOffsetDidChange(change) - - if animationView.isAnimationPlaying { - return - } - - switch state { - case .idle, .pulling: - guard let dic = change else { - return - } - guard let value = dic[NSKeyValueChangeKey.newKey] as? NSValue else { - return - } - - let offsetY = value.cgPointValue.y - let newValue = offsetY - 25 > 0 ? offsetY - 25 : 0 - var progress = abs(newValue) / kMaxFooterPanHeight - progress = progress >= 1 ? 1 : progress - //dlog(progress) - // * 一个系数,免得动画太快 - animationView.currentProgress = progress * 0.4 - animationView.alpha = progress + 0.5 - - default: - break - } - } - - override func scrollViewPanStateDidChange(_ change: [AnyHashable: Any]?) { - super.scrollViewPanStateDidChange(change) - if state != .refreshing { - animationView.stop() - } - } -} - -@objcMembers class RefreshTrailerAnimator: MJRefreshStateTrailer { - private lazy var animationView: LottieAnimationView = { - - let animation = LottieAnimation.named("single_ring") - let animationView = LottieAnimationView(animation: animation) - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.size = CGSize(width: 100, height: 100) - animationView.backgroundColor = .clear - addSubview(animationView) - return animationView - }() - - - override func placeSubviews() { - super.placeSubviews() - stateLabel?.isHidden = true - - self.mj_w = 70 - let selfCenter = CGPoint(x: self.mj_w / 2, y: self.mj_h / 2) - animationView.center = selfCenter - } - - override var state: MJRefreshState { - willSet { - guard state != newValue else { - return - } - } - didSet { - super.state = state - - switch state { - case .refreshing: - DispatchQueue.main.async { [weak self] in - self?.animationView.play() - } - - case .pulling: - animationView.currentProgress = 0 - animationView.alpha = 1 - let generator = UIImpactFeedbackGenerator(style: .medium) - generator.prepare() - generator.impactOccurred() - - case .noMoreData: - animationView.currentProgress = 0 - animationView.alpha = 0 - animationView.stop() - - default: - break - } - } - } -} - - - diff --git a/crush/Crush/Src/Components/UI/Sheet/Base/BaseActionSheet.swift b/crush/Crush/Src/Components/UI/Sheet/Base/BaseActionSheet.swift deleted file mode 100644 index 3390640..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/Base/BaseActionSheet.swift +++ /dev/null @@ -1,204 +0,0 @@ -// -// ActionSheet.swift -// Crush -// -// Created by Leon on 2025/7/17. -// - -import UIKit - -// MARK: - Constants -private let kHeightTitle: CGFloat = 60 -private let kHeightButton: CGFloat = 48 -private let kButtonVPadding: CGFloat = 24 -private let kHeightBottomLine: CGFloat = 0 - -// MARK: - EGActionSheetAction - -class EGActionSheetAction { - var title: String - var autoDismiss: Bool - var actionBlock: (() -> Void)? - // Optional - var iconLeading:UIImage? - - init(title: String, iconLeading: UIImage? = nil, autoDismiss: Bool, actionBlock: (() -> Void)?) { - self.title = title - self.iconLeading = iconLeading - self.autoDismiss = autoDismiss - self.actionBlock = actionBlock - } - - static func action(title: String, block: (() -> Void)? = nil) -> EGActionSheetAction { - return action(title: title, autoDismiss: true, block: block) - } - - static func action(title: String, autoDismiss: Bool, block: (() -> Void)?) -> EGActionSheetAction { - return EGActionSheetAction(title: title, autoDismiss: autoDismiss, actionBlock: block) - } -} - -// MARK: - EGActionSheet - -class EGActionSheet: EGPopBaseView { - // MARK: - Properties - private var titleView: UIView! - private var titleLabel: UILabel! - private var closeButton: EPIconGhostButton! - private var btsStackV: UIStackView! - - private var buttons: [UIButton] = [] - private var actions: [EGActionSheetAction] = [] - - var title: String? { - didSet { - titleLabel.text = title - } - } - - var cancelBlock: (() -> Void)? - - // MARK: - Initialization - init() { - super.init(direction: .bottom) - dataInit() - uiInit() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func dataInit() { - contentView.backgroundColor = .c.csbn; //- Replace with EPSystemToken equivalent (EPS_Color_Surface_base_normal) - animateDuration = 0.35 - } - - private func uiInit() { - contentView.layer.cornerRadius = 24 - contentView.layer.masksToBounds = true - contentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - - // Title View - titleView = UIView() - contentView.addSubview(titleView) - titleView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - titleView.heightAnchor.constraint(equalToConstant: 48), - titleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - titleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - titleView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12) - ]) - - // Close Button - closeButton = EPIconGhostButton(radius: .none, iconSize: .small, iconCode: .delete) - closeButton.addTarget(self, action: #selector(cancelButtonPressed(_:)), for: .touchUpInside) - titleView.addSubview(closeButton) - closeButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - closeButton.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -8), - closeButton.centerYAnchor.constraint(equalTo: titleView.centerYAnchor), - closeButton.widthAnchor.constraint(equalToConstant: 48), - closeButton.heightAnchor.constraint(equalToConstant: 48) - ]) - - // Title Label - titleLabel = UILabel() - titleLabel.font = CLSystemToken.font(token: .tts) // (EPS_Txt_Title_s) - titleLabel.textColor = .c.ctpn // (EPS_Color_Txt_primary_normal) - titleLabel.textAlignment = .center - titleView.addSubview(titleLabel) - titleLabel.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: titleView.leadingAnchor, constant: 24), - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: closeButton.leadingAnchor, constant: -8), - titleLabel.centerXAnchor.constraint(equalTo: titleView.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: titleView.centerYAnchor) - ]) - - // Button Stack View - btsStackV = UIStackView() - btsStackV.spacing = 24 - btsStackV.axis = .vertical - contentView.addSubview(btsStackV) - btsStackV.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - btsStackV.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), - btsStackV.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), - btsStackV.topAnchor.constraint(equalTo: titleView.bottomAnchor, constant: 24), - btsStackV.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24 - UIWindow.safeAreaBottom) - ]) - - titleLabel.text = NSLocalizedString("More", comment: "") - } - - // MARK: - Lifecycle - override func willMove(toSuperview newSuperview: UIView?) { - super.willMove(toSuperview: newSuperview) - updateContentHeight() - } - - override func layoutSubviews() { - super.layoutSubviews() - // Additional layout logic if needed - } - - // MARK: - Data - private func updateContentHeight() { - var height: CGFloat = 24 + UIWindow.safeAreaBottom + 24 + (48 + 12) - if !buttons.isEmpty { - height += CGFloat(buttons.count) * kHeightButton - height += CGFloat(buttons.count - 1) * btsStackV.spacing - } - setContentLength(height) - } - - // MARK: - Actions - func addAction(_ action: EGActionSheetAction, propertySetup: ((UIButton) -> Void)? = nil) { - let button = StyleButton(type: .custom) - button.tertiary(size: .large) - button.setTitle(action.title, for: .normal) - button.addTarget(self, action: #selector(buttonPressed(_:)), for: .touchUpInside) - button.tag = actions.count - - if let iconLeading = action.iconLeading{ - let iv = UIImageView() - iv.image = iconLeading - button.addSubview(iv) - iv.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(24) - } - } - - btsStackV.addArrangedSubview(button) - - if let propertySetup = propertySetup { - propertySetup(button) - } - - actions.append(action) - buttons.append(button) - - updateContentHeight() - } - - // MARK: - Events - override func bgButtonPressed(_ button: UIButton) { - cancelBlock?() - super.bgButtonPressed(button) - } - - @objc private func cancelButtonPressed(_ button: UIButton) { - cancelBlock?() - dismiss() - } - - @objc private func buttonPressed(_ button: UIButton) { - let action = actions[button.tag] - if action.autoDismiss { - dismiss() - } - action.actionBlock?() - } -} diff --git a/crush/Crush/Src/Components/UI/Sheet/Base/PopBaseView.swift b/crush/Crush/Src/Components/UI/Sheet/Base/PopBaseView.swift deleted file mode 100644 index 1199dbf..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/Base/PopBaseView.swift +++ /dev/null @@ -1,285 +0,0 @@ -// -// BaseActionSheet.swift -// Crush -// -// Created by Leon on 2025/7/17. -// - -import UIKit - -// MARK: - Enums - -enum EGPopViewDirection: Int { - case top - case left - case right - case bottom -} - -// MARK: - Protocols - -protocol EGPopViewDelegate: AnyObject { - func popViewWillAppear(_ view: EGPopBaseView) - func popViewDidAppear(_ view: EGPopBaseView) - func popViewWillDisappear(_ view: EGPopBaseView) - func popViewDidDisappear(_ view: EGPopBaseView) -} - -extension EGPopViewDelegate { - func popViewWillAppear(_ view: EGPopBaseView) {} - func popViewDidAppear(_ view: EGPopBaseView) {} - func popViewWillDisappear(_ view: EGPopBaseView) {} - func popViewDidDisappear(_ view: EGPopBaseView) {} -} - -// MARK: - Main Class - -class EGPopBaseView: UIView { - // MARK: - Properties - private var effectView: UIVisualEffectView - private var backgroundView: UIButton - private(set) var contentView: UIView - private var direction: EGPopViewDirection - - /// has value when keyboard show! - public var bottomKeyboardHeight: CGFloat = 0 - - weak var delegate: EGPopViewDelegate? - - var bgAlpha: CGFloat = 0.6 { - didSet { - backgroundView.alpha = dimBg ? bgAlpha : 0 - } - } - - var bgColor: UIColor = .black { - didSet { - backgroundView.backgroundColor = bgColor - } - } - - var dimBg: Bool = true { - didSet { - backgroundView.alpha = dimBg ? bgAlpha : 0 - } - } - - var blurBg: Bool = false { - didSet { - effectView.effect = blurBg ? UIBlurEffect(style: .extraLight) : nil - } - } - - public var contentLength: CGFloat = 0 { - didSet { - layoutContentViewToHide() - } - } - - var contentInsets: UIEdgeInsets = .zero { - didSet { - layoutContentViewToHide() - } - } - - var animateDuration: CGFloat = 0.4 - - // MARK: - Initialization - init(direction: EGPopViewDirection) { - self.direction = direction - self.effectView = UIVisualEffectView(effect: nil) - self.backgroundView = UIButton(type: .custom) - self.contentView = UIView() - super.init(frame: .zero) - - backgroundColor = .clear - setupEffectView() - setupBackgroundView() - setupContentViewForDirection(direction) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupEffectView() { - effectView.frame = UIScreen.main.bounds - addSubview(effectView) - } - - private func setupBackgroundView() { - bgColor = .black.withAlphaComponent(1) // Replace with EPSystemToken equivalent - backgroundView.addTarget(self, action: #selector(bgButtonPressed(_:)), for: .touchUpInside) - backgroundView.frame = UIScreen.main.bounds - backgroundView.backgroundColor = bgColor - backgroundView.alpha = 0 - addSubview(backgroundView) - } - - private func setupContentViewForDirection(_ direction: EGPopViewDirection) { - contentView.backgroundColor = .white - contentView.layer.cornerRadius = 32 - contentView.layer.masksToBounds = true - contentView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - addSubview(contentView) - - switch direction { - case .top, .bottom: - contentLength = UIScreen.main.bounds.height / 3 - case .left, .right: - contentLength = UIScreen.main.bounds.width / 3 - } - } - - // MARK: - Data - func setContentLength(_ length: CGFloat) { - contentLength = length - } - - // MARK: - Events - @objc func bgButtonPressed(_ button: UIButton) { - dismiss() - } - - // MARK: - Layout - func layoutInWindow() { - guard let window = UIWindow.getTopDisplayWindow() else { return } - window.endEditing(true) - window.addSubview(self) - self.frame = window.bounds - } - - private func layoutContentViewToHide() { - let screenWidth = UIScreen.main.bounds.width - let screenHeight = UIScreen.main.bounds.height - - switch direction { - case .top: - contentView.frame = CGRect( - x: contentInsets.left, - y: -(contentLength + contentInsets.top), - width: screenWidth - (contentInsets.left + contentInsets.right), - height: contentLength - ) - case .bottom: - contentView.frame = CGRect( - x: contentInsets.left, - y: screenHeight + contentInsets.bottom, - width: screenWidth - (contentInsets.left + contentInsets.right), - height: contentLength - ) - case .left: - contentView.frame = CGRect( - x: -(contentLength + contentInsets.left), - y: contentInsets.top, - width: contentLength, - height: screenHeight - (contentInsets.top + contentInsets.bottom) - ) - case .right: - contentView.frame = CGRect( - x: screenWidth + contentInsets.right, - y: contentInsets.top, - width: contentLength, - height: screenHeight - (contentInsets.top + contentInsets.bottom) - ) - } - } - - private func layoutContentViewToDisplay() { - let screenWidth = UIScreen.main.bounds.width - let screenHeight = UIScreen.main.bounds.height - - switch direction { - case .top: - contentView.frame = CGRect( - x: contentInsets.left, - y: contentInsets.top, - width: screenWidth - (contentInsets.left + contentInsets.right), - height: contentLength - ) - case .bottom: - contentView.frame = CGRect( - x: contentInsets.left, - y: screenHeight - contentLength - contentInsets.bottom - bottomKeyboardHeight, - width: screenWidth - (contentInsets.left + contentInsets.right), - height: contentLength - ) - case .left: - contentView.frame = CGRect( - x: contentInsets.left, - y: contentInsets.top, - width: contentLength, - height: screenHeight - (contentInsets.top + contentInsets.bottom) - ) - case .right: - contentView.frame = CGRect( - x: screenWidth - contentLength - contentInsets.right, - y: contentInsets.top, - width: contentLength, - height: screenHeight - (contentInsets.top + contentInsets.bottom) - ) - } - } - - // MARK: - Animation - func showWithAnimation() { - delegate?.popViewWillAppear(self) - - UIView.animate(withDuration: TimeInterval(animateDuration), delay: 0, options: .curveEaseInOut) { [weak self] in - guard let self = self else { return } - if self.dimBg { - self.backgroundView.alpha = self.bgAlpha - } - if self.blurBg { - self.effectView.effect = UIBlurEffect(style: .extraLight) - } - self.layoutContentViewToDisplay() - } completion: { [weak self] finished in - guard let self = self else { return } - self.delegate?.popViewDidAppear(self) - } - } - - func dismissWithAnimation() { - delegate?.popViewWillDisappear(self) - - UIView.animate(withDuration: TimeInterval(animateDuration), delay: 0, options: .curveEaseInOut) { [weak self] in - guard let self = self else { return } - if self.dimBg { - self.backgroundView.alpha = 0 - } - if self.blurBg { - self.effectView.effect = nil - } - self.layoutContentViewToHide() - } completion: { [weak self] finished in - guard let self = self else { return } - self.delegate?.popViewDidDisappear(self) - self.removeFromSuperview() - } - } - - // MARK: - Display - func show() { - layoutInWindow() - showWithAnimation() - } - - func dismiss() { - dismissWithAnimation() - } - - func refreshLPositon(){ - layoutContentViewToDisplay() - } - - static func hideAllPopView() { - guard let window = UIWindow.getTopDisplayWindow() else { return } - for subview in window.subviews { - if let popView = subview as? EGPopBaseView { - popView.isHidden = true - popView.removeFromSuperview() - } - } - } -} diff --git a/crush/Crush/Src/Components/UI/Sheet/BirthdayPickerView.swift b/crush/Crush/Src/Components/UI/Sheet/BirthdayPickerView.swift deleted file mode 100644 index 3952232..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/BirthdayPickerView.swift +++ /dev/null @@ -1,187 +0,0 @@ -// -// BirthdayPickerView.swift -// Crush -// -// Created by Leon on 2025/7/19. -// - -import SnapKit -import UIKit - -class BirthdayPickerView: EGPopBaseView { - // MARK: - Properties - - private var titleLabel: UILabel - let pickerView: CLDatePicker - let confirmButton: TickButton - private var closeButton: EPIconGhostButton - private var defaultDate: Date? - - var selectBirthday: ((Date, Int) -> Void)? - var tapCancelAction: (() -> Void)? - var tapConfirmAction: ((Date, Int) -> Void)? - - var optionMinimumDate: Date? { - didSet { - if let minimumDate = optionMinimumDate { - pickerView.minimumDate = minimumDate - } - } - } - - init() { - titleLabel = UILabel() - pickerView = CLDatePicker() - confirmButton = TickButton() - closeButton = EPIconGhostButton(radius: .none, iconSize: .small, iconCode: .delete) - - super.init(direction: .bottom) - - contentView.backgroundColor = .c.csbn - - // 默认选择的最小年月和默认年月 - optionMinimumDate = Date(year: 1960, month: 1, day: 1) - pickerView.setDate(Date(year: 2000, month: 1, day: 1), animated: false) - - initialData() - initialViews() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - private func initialData() { - contentLength = 268 + UIWindow.safeAreaBottom - } - - private func initialViews() { - // Title view - let titleView = UIView() - titleView.backgroundColor = .clear - contentView.addSubview(titleView) - titleView.snp.makeConstraints { make in - make.left.right.equalTo(contentView) - make.top.equalTo(contentView).offset(12) - make.height.equalTo(48) - } - - // Close button - closeButton.addTarget(self, action: #selector(cancelAction), for: .touchUpInside) - titleView.addSubview(closeButton) - closeButton.snp.makeConstraints { make in - make.centerY.equalTo(titleView) - make.left.equalTo(titleView).offset(12) - make.size.equalTo(CGSize(width: 48, height: 48)) - } - - // Confirm button - confirmButton.isEnabled = true - confirmButton.addTarget(self, action: #selector(confirmAction), for: .touchUpInside) - titleView.addSubview(confirmButton) - confirmButton.snp.makeConstraints { make in - make.centerY.equalTo(titleView) - make.right.equalTo(titleView).offset(-24) - } - - // Title label - titleLabel.font = CLSystemToken.font(token: .tts) // EPSystemToken.typography(.txtTitleS).font - titleLabel.textColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - titleLabel.textAlignment = .center - titleView.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.center.equalTo(titleView) - } - titleLabel.text = NSLocalizedString("select", comment: "") - - // Date picker - if #available(iOS 13.4, *) { - pickerView.preferredDatePickerStyle = .wheels - } - contentView.addSubview(pickerView) - pickerView.snp.makeConstraints { make in - make.left.right.equalTo(contentView) - make.top.equalTo(titleView.snp.bottom) - make.bottom.equalTo(contentView).offset(-UIWindow.safeAreaBottom) - } - pickerView.datePickerMode = .date - pickerView.maximumDate = Date() - pickerView.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) - - // 设置 textColor - pickerView.setValue(UIColor.c.ctpn, forKey: "textColor") - - // 调用 setHighlightsToday:(私有 API,注意 App Store 风险) - let selector = Selector(("setHighlightsToday:")) - if pickerView.responds(to: selector) { - pickerView.perform(selector, with: false) - } - -// // Theming for picker view -// pickerView.setValue(UIColor.cl.ctpn, forKey: "textColor") -// if let selector = NSSelectorFromString("setHighlightsToday:") { -// var no = false -// let invocation = NSInvocation.invocation(withMethodSignature: UIDatePicker.instanceMethodSignature(for: selector)) -// invocation.selector = selector -// invocation.setArgument(&no, atIndex: 2) -// invocation.invoke(withTarget: item) -// } - - // Set default minimum date - pickerView.minimumDate = Date(year: 1950, month: 1, day: 1) - - titleLabel.textColor = .c.ctpn // EPSystemToken.color(.txtPrimaryNormal) - } - - // MARK: - Actions - - @objc private func cancelAction() { - tapCancelAction?() - if superview != nil { - dismiss() - } - } - - @objc private func confirmAction() { - defaultDate = pickerView.date - let timestamp = Int(pickerView.date.timeIntervalSince1970) - tapConfirmAction?(pickerView.date, timestamp) - if superview != nil { - dismiss() - } - } - - @objc private func dateChanged(_ datePicker: UIDatePicker) { - let date = datePicker.date - let timestamp = Int(date.timeIntervalSince1970) - selectBirthday?(date, timestamp) - } - - // MARK: - Public Methods - - func setupDefaultSelectDate(_ date: Date?) { - guard let date = date else { return } - defaultDate = date - pickerView.setDate(date, animated: false) - } -} - -// MARK: - Extensions - -extension Date { - init(year: Int, month: Int, day: Int) { - let calendar = Calendar.current - var components = DateComponents() - components.year = year - components.month = month - components.day = day - self = calendar.date(from: components) ?? Date() - } - -// var timestamp: Int64 { -// Int64(timeIntervalSince1970) -// } -} diff --git a/crush/Crush/Src/Components/UI/Sheet/BuyCreditsSheetView.swift b/crush/Crush/Src/Components/UI/Sheet/BuyCreditsSheetView.swift deleted file mode 100644 index 7bd4ef2..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/BuyCreditsSheetView.swift +++ /dev/null @@ -1,425 +0,0 @@ -// -// BuyCreditsSheetView.swift -// Crush -// -// Created by Leon on 2025/7/23. -// - -import UIKit -import Combine -/// 额外购买次数 -class BuyCreditsSheetView: EGPopBaseView { - var closeButton: EPIconTertiaryButton! - var logo :UIImageView! - var titleLabel:UILabel! - - - var stackV : UIStackView! - // content 1: numberscontent - var priceLabel: UILabel! - - // content 2: - var addButton: InDecreaseButton! - /// 1-999 - var inputBox:NumbersInputBox! - var decreaseButton: InDecreaseButton! - - // content 3: total Price - var totalPriceLabel: UILabel! - - var buyButton:StyleButton! - - // Data - - var maxNum = 999 - var minNum = 1 - - /// 单价(每次价格) - var pricePer: Int = 7000 - @Published var num: Int = 10 - private var cancellables = Set() - - init() { - super.init(direction: .bottom) - contentView.backgroundColor = .c.csbn - contentLength = 444 + UIWindow.safeAreaBottom - - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - logo = { - let v = UIImageView() - contentView.addSubview(v) - v.image = UIImage(named: "buy_credits_title_logo") - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 100, height: 100)) - make.centerX.equalToSuperview() - make.top.equalToSuperview().offset(24) - } - return v - }() - - titleLabel = { - let v = UILabel() - contentView.addSubview(v) - v.text = "Buy Credits" - v.font = .t.ttm - v.textColor = .text - v.textAlignment = .center - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(logo.snp.bottom).offset(24) - } - v.numberOfLines = 0 - return v - }() - - stackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 0 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(titleLabel.snp.bottom).offset(24) - } - return v - }() - - do{ - let priceView = { - let v = UIView() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(48) - } - return v - }() - - let tipLabel = { - let label = UILabel() - label.font = .t.tll - label.textColor = .text - priceView.addSubview(label) - label.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return label - }() - tipLabel.text = "Price" - - priceLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - let coinIv = { - let v = UIImageView() - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(priceLabel.snp.leading).offset(-8) - make.centerY.equalToSuperview() - } - return v - }() - coinIv.image = UIImage(named: "icon_16_diamond") - } - - setupQuantityView() - - do{ - let priceView = { - let v = UIView() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(48) - } - return v - }() - - let tipLabel = { - let label = UILabel() - label.font = .t.tll - label.textColor = .text - priceView.addSubview(label) - label.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return label - }() - tipLabel.text = "Total Price" - - totalPriceLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - v.text = "0" - return v - }() - - let coinIv = { - let v = UIImageView() - v.image = UIImage(named: "icon_16_diamond") - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(totalPriceLabel.snp.leading).offset(-8) - make.centerY.equalToSuperview() - } - return v - }() - coinIv.isHidden = false - } - - - buyButton = { - let v = StyleButton() - v.primary(size: .large) - contentView.addSubview(v) - v.addTarget(self, action: #selector(submitAction), for: .touchUpInside) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - //make.top.equalTo(titleLabel.snp.bottom).offset(24) - make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) - } - return v - }() - buyButton.setTitle("Confirm", for: .normal) - - setupData() - setupEvent() - -//#warning("test") -// testData() - } - - private func setupQuantityView(){ - let quantityView = { - let v = UIView() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(72) - } - return v - }() - - let quantityLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - quantityView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - quantityLabel.text = "Quantity" - - addButton = { - let v = InDecreaseButton() - v.setupIncreaseState() - quantityView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - v.addTarget(self, action: #selector(tapIncreaseButton), for: .touchUpInside) - return v - }() - - inputBox = { - let v = NumbersInputBox() - quantityView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(addButton.snp.leading).offset(-8) - make.centerY.equalToSuperview() - } - //v.text = "0" - v.placeholder = "0" - return v - }() - - decreaseButton = { - let v = InDecreaseButton() - v.setupDecreaseState() - quantityView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(inputBox.snp.leading).offset(-8) - make.centerY.equalToSuperview() - } - - v.addTarget(self, action: #selector(tapDecreaseButton), for: .touchUpInside) - return v - }() - } - - private func setupData(){ - - // 单价 - let price = Coin(cents: 70*100) - let priceLabelText = "\(price.formatted)" - priceLabel.text = priceLabelText - } - - private func setupEvent(){ - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChanged(noti:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - - inputBox.textDidChanged = {[weak self] textField in - guard let self = self else{return} - let count = Int(textField.text ?? "1") ?? 0 - - self.num = count - if count > self.maxNum{ - textField.text = "\(self.maxNum)" - self.num = self.maxNum - } - - self.buyButton.isEnabled = count > 0 - } - - inputBox.textDidEndEditing = {[weak self] textField in - guard let self = self else{return} - let count = Int(textField.text ?? "1") ?? 1 - self.num = count - if count < self.minNum{ - textField.text = "\(self.minNum)" - self.num = self.minNum - } - } - - $num.sink {[weak self] count in - guard let self = self else{return} - - self.inputBox.text = "\(count)" - - let total = count * self.pricePer - let totalCoin = Coin(cents: total) - totalPriceLabel.text = totalCoin.formatted - - self.decreaseButton.isEnabled = count > minNum - self.addButton.isEnabled = count < maxNum - - - - }.store(in: &cancellables) - - } - - private func testData(){ -// let price = Coin(cents: 70*100) -// let priceLabelText = "\(price.formatted) USD" -// priceLabel.text = priceLabelText - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func show() { - super.show() - } - - // MARK: - Action - - @objc func submitAction(){ - -// let alert = Alert(title: "Buy Credits", text: "Are you sure you want to buy credits?") -// let action1 = AlertAction(title: "Cancel", actionStyle: .cancel) -// let action2 = AlertAction(title: "Buy", actionStyle: .destructive) -// alert.addAction(action1) -// alert.addAction(action2) -// alert.show() - - doPurchase() - } - - @objc private func tapDecreaseButton(){ - num -= 1 - } - - @objc private func tapIncreaseButton(){ - num += 1 - } - - - // MARK: - Functions - - private func doPurchase(){ -// #warning("test") -// dismiss() -// let buyCoinSheet = CoinsRechargeSheet() -// buyCoinSheet.show() -// return - - let count = num - // 创作次数+X - Hud.showIndicator() - UserProvider.request(.buyUserCreateCount(count: num), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - Hud.toast(str: "创作次数+\(count)") {[weak self] complete in - self?.dismiss() - } - NotificationCenter.post(name: .buyCreditsOnce) - case .failure(let failure): - switch failure { - case let .serviceError(code, _): - if code == .insufficentCoin{ - self?.dismiss() - // 遇到此错误吗会会统一弹出sheet去充值 -// let buyCoinSheet = CoinsRechargeSheet() -// buyCoinSheet.show() - } - default: - break - } - break - } - } - } - - - // MARK: - Helper - @objc func keyboardWillChanged(noti: Notification) { - guard let userInfo = noti.userInfo else { return } - //let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval - let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue - - if endFrame.origin.y >= UIScreen.main.bounds.height { - bottomKeyboardHeight = 0 - refreshLPositon() - }else{ - bottomKeyboardHeight = endFrame.height - refreshLPositon() - } - } - -} diff --git a/crush/Crush/Src/Components/UI/Sheet/ChatModePickInsufficientCoinSheet.swift b/crush/Crush/Src/Components/UI/Sheet/ChatModePickInsufficientCoinSheet.swift deleted file mode 100644 index 53dfb49..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/ChatModePickInsufficientCoinSheet.swift +++ /dev/null @@ -1,303 +0,0 @@ -// -// ChatModePickInsufficientCoinSheet.swift -// Crush -// -// Created by Leon on 2025/9/23. -// -import UIKit -import Combine - -/// 余额不足,对话模型选择 -class ChatModePickInsufficientCoinSheet: EGPopBaseView { - var closeButton: EPIconTertiaryButton! - var titleLabel: UILabel! - var subTitleLabel: LineSpaceLabel! - - // 滚动视图 - var scrollView: UIScrollView! - - // 模型卡片视图数组 - var modelCardViews: [ChatModelCardView] = [] - - var bottomView: UIView! - var balance: CLIconLabel! - var operateButton: StyleButton! // Recharge - - // 数据源 - private var chatModels: [AIChatModel] = [] - private var currentSelectedModelCode: String? - - var cardHeight = 168.0 - // 选择回调 - var selectionCallback: ChatModelSelectionCallback? - - private var cancellables = Set() - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - init(currentSelectedModelCode: String? = nil) { - self.currentSelectedModelCode = currentSelectedModelCode - super.init(direction: .bottom) - - contentView.backgroundColor = .c.csbn - // 初始高度,后续会根据数据动态调整 - contentLength = 200 + UIWindow.safeAreaBottom - - setupViews() - setupDatas() - setupEvent() - } - - private func setupViews() { - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .text - v.textAlignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(32) - } - return v - }() - - subTitleLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbs) - v.config(typo) - v.textColor = .c.ctpn - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(24) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - scrollView = { - let v = UIScrollView() - v.showsVerticalScrollIndicator = false - v.showsHorizontalScrollIndicator = false - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(subTitleLabel.snp.bottom).offset(16) // 24 - make.height.equalTo(cardHeight) // 每个卡片的高度 - } - return v - }() - - bottomView = { - let v = UIView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - make.leading.trailing.equalToSuperview() - make.height.equalTo(80) - } - return v - }() - - balance = { - let v = CLIconLabel() - v.iconSize = CGSize(width: 16, height: 16) - v.iconImageView.image = .icon16Diamond - v.contentLabel.textColor = .white - v.contentLabel.font = .t.tlm - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(24) - } - return v - }() - - operateButton = { - let v = StyleButton() - v.primary(size: .large) - v.addTarget(self, action: #selector(operateButtonTapped), for: .touchUpInside) - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-24) - make.leading.greaterThanOrEqualTo(balance.snp.trailing).offset(48) - } - return v - }() - - titleLabel.text = "Dialog Model" - subTitleLabel.text = "The Crush coin is insufficient and cannot continue to send messages. The charging standard of the current model:" - operateButton.setTitle("Recharge", for: .normal) - - - } - - private func setupDatas(){ -// let group = DispatchGroup() -// -// group.enter() - loadChatModels { -// group.leave() - } - -// group.enter() -// loadChatSetting { -// group.leave() -// } -// -// group.notify(queue: .main) {[weak self] in -// self?.setupModelCards() -// } - } - - private func setupEvent(){ - WalletCore.shared.$balance.sink {[weak self] balance in - if let priceLabel = self?.balance { - priceLabel.contentLabel.text = balance.displayBalance() - } - }.store(in: &cancellables) - } - - private func loadChatModels(completion:(()->Void)? = nil) { - guard let models = AppDictManager.shared.chatModels, models.count > 0 else { - // 如果没有数据,尝试加载 - AppDictManager.shared.loadChatModelDict { [weak self] success in - if success { - self?.loadChatModels() - } - } - return - } - - chatModels = models - setupModelCards() - completion?() - } - - private func setupModelCards() { - // 清除之前的卡片 - modelCardViews.forEach { $0.removeFromSuperview() } - modelCardViews.removeAll() - - // 创建新的卡片 - for (index, model) in chatModels.enumerated() { - let cardView = ChatModelCardView() - cardView.configure(with: model, isSelected: model.code == currentSelectedModelCode) - cardView.tag = index - cardView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(modelCardTapped(_:)))) - - scrollView.addSubview(cardView) - modelCardViews.append(cardView) - - cardView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.width.equalTo(scrollView.snp.width) - make.height.equalTo(cardHeight) // 每个卡片固定高度 - if index == 0 { - make.top.equalToSuperview() - } else { - make.top.equalTo(modelCardViews[index - 1].snp.bottom).offset(12) - } - if index == chatModels.count - 1 { - make.bottom.equalToSuperview() - } - } - } - - // 动态调整contentLength - updateContentLength() - - // 延迟执行滚动操作,确保视图布局完成 -// DispatchQueue.main.async { [weak self] in -// self?.scrollToSelectedModel() -// } - } - - private func updateContentLength() { - let subTitleHeight = subTitleLabel.sizeThatFits(CGSize(width: UIScreen.width - CGFloat.lrs*2, height: CGFLOAT_MAX)).height - //dlog("文字高度: \(subTitleHeight)") - // 基础高度:标题 + 间距 + 底部标签 - let baseHeight: CGFloat = 80 + subTitleHeight + 80 + 16// titleLabel top + spacing + stayTunedLabel + bottom margin - - // 卡片高度:每个卡片200 + 间距12(除了最后一个) - let cardHeight: CGFloat = cardHeight - let cardSpacing: CGFloat = 12 - let totalCardHeight = CGFloat(chatModels.count) * cardHeight + CGFloat(max(0, chatModels.count - 1)) * cardSpacing - - // 滚动视图高度:根据卡片数量调整,最多显示2个卡片的高度 - let maxVisibleHeight = min(totalCardHeight, cardHeight * 2 + cardSpacing) - - // 更新scrollView高度约束 - scrollView.snp.updateConstraints { make in - make.height.equalTo(maxVisibleHeight) - } - - // 更新contentLength - contentLength = baseHeight + maxVisibleHeight + UIWindow.safeAreaBottom - - // 更新布局 - layoutIfNeeded() - } - - // MARK: - Function - private func scrollToSelectedModel() { - guard let selectedCode = currentSelectedModelCode, - let selectedIndex = chatModels.firstIndex(where: { $0.code == selectedCode }), - selectedIndex < chatModels.count else { return } - - // 确保scrollView已经有正确的frame - guard scrollView.frame.height > 0 else { - // 如果frame还没有设置,再次延迟执行 - DispatchQueue.main.async { [weak self] in - self?.scrollToSelectedModel() - } - return - } - - let offsetY = CGFloat(selectedIndex) * (cardHeight + 12) // 卡片高度 + 间距 - scrollView.setContentOffset(CGPoint(x: 0, y: offsetY), animated: false) - } - - // MARK: - Action - @objc private func modelCardTapped(_ gesture: UITapGestureRecognizer) { - guard let cardView = gesture.view as? ChatModelCardView, - let index = modelCardViews.firstIndex(of: cardView), - index < chatModels.count else { return } - - let selectedModel = chatModels[index] - - modelCardViews.forEach { $0.setSelected(false) } - cardView.setSelected(true) - - // 调用回调 - selectionCallback?(selectedModel) - - IMAIViewModel.shared.updateChatModel(code: selectedModel.code, completion: nil) - - // 关闭弹窗 - dismiss() - } - - @objc private func operateButtonTapped(){ - dismiss() - AppRouter.goWalletCenter() - } -} diff --git a/crush/Crush/Src/Components/UI/Sheet/ChatModePickSheet.swift b/crush/Crush/Src/Components/UI/Sheet/ChatModePickSheet.swift deleted file mode 100644 index 57f0d36..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/ChatModePickSheet.swift +++ /dev/null @@ -1,401 +0,0 @@ -// -// ChatModePickSheet.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit - -/// 对话模型选择回调 -typealias ChatModelSelectionCallback = (AIChatModel) -> Void - -/// 对话模型选择 -class ChatModePickSheet: EGPopBaseView { - var closeButton: EPIconTertiaryButton! - var titleLabel: UILabel! - - // 滚动视图 - var scrollView: UIScrollView! - - // 模型卡片视图数组 - var modelCardViews: [ChatModelCardView] = [] - - var stayTunedLabel: CLLabel! - - // 数据源 - private var chatModels: [AIChatModel] = [] - private var currentSelectedModelCode: String? - - var cardHeight = 200.0//168.0 - // 选择回调 - var selectionCallback: ChatModelSelectionCallback? - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - init(currentSelectedModelCode: String? = nil) { - self.currentSelectedModelCode = currentSelectedModelCode - super.init(direction: .bottom) - - contentView.backgroundColor = .c.csbn - // 初始高度,后续会根据数据动态调整 - contentLength = 200 + UIWindow.safeAreaBottom - - setupViews() - loadChatModels() - } - - private func setupViews() { - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .text - v.textAlignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(32) - } - return v - }() - - scrollView = { - let v = UIScrollView() - v.showsVerticalScrollIndicator = false - v.showsHorizontalScrollIndicator = false - v.delegate = self - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(24) - make.height.equalTo(cardHeight) // 每个卡片的高度 - } - return v - }() - - stayTunedLabel = { - let v = CLLabel() - v.font = .t.tbs - v.textColor = .c.ctsn - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalTo(scrollView.snp.bottom).offset(16) - } - return v - }() - - titleLabel.text = "Dialog Model" - stayTunedLabel.text = "Stay tuned for more models" - } - - private func loadChatModels() { - guard let models = AppDictManager.shared.chatModels, models.count > 0 else { - // 如果没有数据,尝试加载 - AppDictManager.shared.loadChatModelDict { [weak self] success in - if success { - self?.loadChatModels() - } - } - return - } - - chatModels = models - setupModelCards() - } - - private func setupModelCards() { - // 清除之前的卡片 - modelCardViews.forEach { $0.removeFromSuperview() } - modelCardViews.removeAll() - - // 创建新的卡片 - for (index, model) in chatModels.enumerated() { - let cardView = ChatModelCardView() - cardView.configure(with: model, isSelected: model.code == currentSelectedModelCode) - cardView.tag = index - cardView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(modelCardTapped(_:)))) - - scrollView.addSubview(cardView) - modelCardViews.append(cardView) - - cardView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.width.equalTo(scrollView.snp.width) - make.height.equalTo(cardHeight) // 每个卡片固定高度 - if index == 0 { - make.top.equalToSuperview() - } else { - make.top.equalTo(modelCardViews[index - 1].snp.bottom).offset(12) - } - if index == chatModels.count - 1 { - make.bottom.equalToSuperview() - } - } - } - - // 动态调整contentLength - updateContentLength() - - // 延迟执行滚动操作,确保视图布局完成 -// DispatchQueue.main.async { [weak self] in -// self?.scrollToSelectedModel() -// } - } - - private func updateContentLength() { - // 基础高度:标题 + 间距 + 底部标签 - let baseHeight: CGFloat = 32 + 24 + 16 + 20 + UIWindow.safeAreaBottom// titleLabel top + spacing + stayTunedLabel + bottom margin - - // 卡片高度:每个卡片200 + 间距12(除了最后一个) - let cardHeight: CGFloat = cardHeight - let cardSpacing: CGFloat = 12 - let totalCardHeight = CGFloat(chatModels.count) * cardHeight + CGFloat(max(0, chatModels.count - 1)) * cardSpacing - - // 滚动视图高度:根据卡片数量调整,最多显示2个卡片的高度 - let maxVisibleHeight = min(totalCardHeight, cardHeight * 2 + cardSpacing) - - // 更新scrollView高度约束 - scrollView.snp.updateConstraints { make in - make.height.equalTo(maxVisibleHeight) - } - - // 更新contentLength - contentLength = baseHeight + maxVisibleHeight + UIWindow.safeAreaBottom - - // 更新布局 - layoutIfNeeded() - } - - private func scrollToSelectedModel() { - guard let selectedCode = currentSelectedModelCode, - let selectedIndex = chatModels.firstIndex(where: { $0.code == selectedCode }), - selectedIndex < chatModels.count else { return } - - // 确保scrollView已经有正确的frame - guard scrollView.frame.height > 0 else { - // 如果frame还没有设置,再次延迟执行 - DispatchQueue.main.async { [weak self] in - self?.scrollToSelectedModel() - } - return - } - - let offsetY = CGFloat(selectedIndex) * (cardHeight + 12) // 卡片高度 + 间距 - scrollView.setContentOffset(CGPoint(x: 0, y: offsetY), animated: false) - } - - @objc private func modelCardTapped(_ gesture: UITapGestureRecognizer) { - guard let cardView = gesture.view as? ChatModelCardView, - let index = modelCardViews.firstIndex(of: cardView), - index < chatModels.count else { return } - - let selectedModel = chatModels[index] - - // 更新选中状态 - modelCardViews.forEach { $0.setSelected(false) } - cardView.setSelected(true) - - // 调用回调 - selectionCallback?(selectedModel) - - // 关闭弹窗 - dismiss() - } -} - -// MARK: - UIScrollViewDelegate -extension ChatModePickSheet: UIScrollViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - // 垂直滚动不需要页面指示器逻辑 - } -} - -// MARK: - ChatModelCardView -class ChatModelCardView: UIView { - private var block: UIView! - private var modelName: UILabel! - private var modelDesc: UILabel! - private var selectMark: UIImageView! - private var innerBlock: UIView! - private var queryButton: EPIconTertiaryButton! - private var priceStackView: UIStackView! - - private var model: AIChatModel? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.backgroundColor = .c.csen - v.cornerRadius = 16 - addSubview(v) - v.snp.makeConstraints { make in -// make.edges.equalToSuperview() - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.bottom.equalToSuperview() - } - return v - }() - - selectMark = { - let v = UIImageView() - v.image = UIImage(named: "checkmark_tick") - v.isHidden = true - block.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 20, height: 20)) - make.top.equalToSuperview().offset(18) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - modelName = { - let v = CLLabel() - v.font = .t.tts - v.setContentCompressionResistancePriority(UILayoutPriority(749), for: .horizontal) - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - make.trailing.lessThanOrEqualTo(selectMark.snp.leading).offset(-8) - } - return v - }() - - queryButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .xs10, iconCode: .question) - v.addTarget(self, action: #selector(tapQueryButton), for: .touchUpInside) - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(modelName.snp.trailing).offset(4) - make.centerY.equalTo(modelName) - make.trailing.lessThanOrEqualToSuperview().offset(-16) - } - return v - }() - - modelDesc = { - let v = CLLabel() - v.font = .t.tbs - v.textColor = .c.ctsn - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(modelName.snp.bottom).offset(4) - } - return v - }() - - innerBlock = { - let v = UIView() - v.backgroundColor = .c.csdn - v.cornerRadius = 12 - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - //make.top.equalTo(modelDesc.snp.bottom).offset(12) - make.top.equalToSuperview().offset(76) - make.height.equalTo(108) // 76 - //make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - priceStackView = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 12 - v.alignment = .leading - innerBlock.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(12) - make.trailing.equalToSuperview().offset(-12) - make.top.equalToSuperview().offset(12) - make.bottom.equalToSuperview().offset(-12) - } - return v - }() - } - - func configure(with model: AIChatModel, isSelected: Bool = false) { - self.model = model - - modelName.text = model.name ?? "" - modelDesc.text = model.description ?? "" - - // 清除之前的价格标签 - priceStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - // 添加价格信息 - if let price = model.textPrice { - let coin = Coin(cents: price) - let priceLabel = CLIconLabel() - priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond") - priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/Text Message" - priceStackView.addArrangedSubview(priceLabel) - } - - if let price = model.voicePrice { - let coin = Coin(cents: price) - let priceLabel = CLIconLabel() - priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond") - priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/Send or play voice" - priceStackView.addArrangedSubview(priceLabel) - } - - if let price = model.voiceChatPrice { - let coin = Coin(cents: price) - let priceLabel = CLIconLabel() - priceLabel.iconImageView.image = UIImage(named: "icon_16_diamond") - priceLabel.contentLabel.text = "\(coin.thousandthsFormatted)/ 1 min Voice call" - priceStackView.addArrangedSubview(priceLabel) - } - - setSelected(isSelected) - } - - func setSelected(_ selected: Bool) { - selectMark.isHidden = !selected -// block.layer.borderWidth = selected ? 2 : 0 -// block.layer.borderColor = selected ? UIColor.c.cpn.cgColor : UIColor.clear.cgColor - } - - @objc func tapQueryButton(){ - let content = "*文本消息价格是指与角色进行文本消息对话的价格,含发送文本,发送图片,发送礼物;按条计算\n\n*发送语音消息价格是指与角色发送语音或者播放角色的语音的价格;按次计算\n\n*语音通话消息价格是指与角色进行语音电话对话的价格;按分钟计算" - let alert = Alert(title: "Tips", text: content) - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) { - - } - alert.addAction(action1) - alert.show() - } -} diff --git a/crush/Crush/Src/Components/UI/Sheet/GiftAIRoleSheet.swift b/crush/Crush/Src/Components/UI/Sheet/GiftAIRoleSheet.swift deleted file mode 100644 index f332985..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/GiftAIRoleSheet.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// GiftAIRoleSheet.swift -// Crush -// -// Created by Leon on 2025/9/13. -// - -class GiftAIRoleSheet: EGPopBaseView{ - var titleLabel:UILabel! - var descLabel : CLLabel! - var closeButton: EPIconTertiaryButton! - var giftView : GiftGridSendView! - - init(){ - super.init(direction: .bottom) - - contentView.backgroundColor = .c.csbn - contentLength = 80 + 32 + 280 + UIWindow.safeAreaBottom - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - - - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .text - v.textAlignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(32) - } - return v - }() - - descLabel = { - let v = CLLabel() - v.font = .t.tbs - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - //make.top.equalTo(titleLabel.snp.bottom).offset(24) - //make.top.equalToSuperview().offset(80) - make.centerY.equalTo(contentView.snp.top).offset(80+10) - } - return v - }() - - giftView = { - let v = GiftGridSendView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - //make.top.equalTo(descLabel.snp.bottom).offset(12) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - } - return v - }() - - titleLabel.text = "Gift" - descLabel.text = "Giving gifts greatly increases the probability of matching" - } - -// override func layoutSubviews() { -// super.layoutSubviews() -// -// dlog("testtttt \(self) giftView:\(String(describing: giftView))") -// if giftView.bounds.size.height > 0{ -// contentLength = giftView.frame.maxY -// } -// } -} diff --git a/crush/Crush/Src/Components/UI/Sheet/RetrieveHeartbeatSheet.swift b/crush/Crush/Src/Components/UI/Sheet/RetrieveHeartbeatSheet.swift deleted file mode 100644 index 4da62fd..0000000 --- a/crush/Crush/Src/Components/UI/Sheet/RetrieveHeartbeatSheet.swift +++ /dev/null @@ -1,332 +0,0 @@ -// -// RetrieveHeartbeatSheet.swift -// Crush -// -// Created by Leon on 2025/8/16. -// - -import UIKit - -class RetrieveHeartbeatSheet: EGPopBaseView { - var closeButton: EPIconTertiaryButton! - var heartWave : HeartBeatAnimationView! - var titleLabel:UILabel! - - - var stackV : UIStackView! - // content 1: numberscontent - var priceLabel: UILabel! - - var quantityContentLabel: UILabel! - - // content 3: total Price - var totalPriceLabel: UILabel! - - var button:StyleButton! - - var aiId: Int? - // @Requrired - var data: AIUserHeartBeatRelation? - - init() { - super.init(direction: .bottom) - contentView.backgroundColor = .c.cbd - contentLength = 444 + UIWindow.safeAreaBottom - - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - heartWave = { - let v = HeartBeatAnimationView() - contentView.insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(-84) - } - return v - }() - - titleLabel = { - let v = UILabel() - contentView.addSubview(v) - v.text = "Retrieve heart value" - v.font = .t.tts - v.textColor = .text - v.textAlignment = .center - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(156) - } - v.numberOfLines = 0 - return v - }() - - stackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 0 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(titleLabel.snp.bottom).offset(24) - } - return v - }() - - do{ - let priceView = { - let v = UIView() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(48) - } - return v - }() - - let tipLabel = { - let label = UILabel() - label.font = .t.tll - label.textColor = .text - priceView.addSubview(label) - label.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return label - }() - tipLabel.text = "Unit price" - - priceLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - let coinIv = { - let v = UIImageView() - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(priceLabel.snp.leading).offset(-8) - make.centerY.equalToSuperview() - } - return v - }() - coinIv.image = UIImage(named: "icon_16_diamond") - } - - setupQuantityView() - - do{ - let priceView = { - let v = UIView() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(48) - } - return v - }() - - let tipLabel = { - let label = UILabel() - label.font = .t.tll - label.textColor = .text - priceView.addSubview(label) - label.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return label - }() - tipLabel.text = "Total" - - totalPriceLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - v.text = "0" - return v - }() - - let coinIv = { - let v = UIImageView() - v.image = UIImage(named: "icon_16_diamond") - priceView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(totalPriceLabel.snp.leading).offset(-8) - make.centerY.equalToSuperview() - } - return v - }() - coinIv.isHidden = false - } - - - button = { - let v = StyleButton() - v.primary(size: .large) - contentView.addSubview(v) - v.setTitle("Purchase", for: .normal) - v.addTarget(self, action: #selector(operateAction), for: .touchUpInside) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - //make.top.equalTo(titleLabel.snp.bottom).offset(24) - make.bottom.equalToSuperview().offset(-16-UIWindow.safeAreaBottom*0.5) - } - return v - }() - - let price = 5 - let priceLabelText = "\(price)" - priceLabel.text = priceLabelText - - setupEvent() - -//#warning("test") -// testData() - } - - private func setupQuantityView(){ - let quantityView = { - let v = UIView() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(72) - } - return v - }() - - let quantityLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - quantityView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - quantityContentLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - quantityView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - quantityLabel.text = "Quantity" - quantityContentLabel.text = "-℃" - - } - - private func setupEvent(){ - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChanged(noti:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - } - - private func testData(){ - let price = 5 - let priceLabelText = "\(price)" - //let coinLabelText = "\(coin) Coins" - priceLabel.text = priceLabelText - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Public - override func show() { - super.show() - } - - func config(_ data: AIUserHeartBeatRelation?){ - self.data = data - - // 单价 - let unitPrice = data?.price ?? 500 - let unitCoin = Coin(cents: unitPrice) - priceLabel.text = unitCoin.formatted - - let heartPercent = data?.getHeartbeatWavePercent() ?? 0.1 - heartWave.percent = heartPercent - - quantityContentLabel.text = data?.fixedSubtractHeartbeatValDisplay ?? "-" - - let quality = data?.fixedSubtractHeartbeatVal ?? 0 - let price = quality * Double(unitPrice) - - var fixPrice = price * 0.01 - if fixPrice > 1{ - fixPrice = floor(fixPrice) - }else{ - fixPrice = max(1, fixPrice) - } - - let coin = Coin(usd: fixPrice) - totalPriceLabel.text = coin.formatted - } - - // MARK: - Action - @objc func operateAction(){ -// guard let aiId = data - guard let subtractHeartbeatVal = data?.subtractHeartbeatVal, let theAiId = aiId else{ - return - } - Hud.showIndicator() - - AIRoleProvider.request(.heartBeatPurchase(aiId: theAiId, heartBeatVal: subtractHeartbeatVal), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - NotificationCenter.post(name: .aiRoleRelationInfoUpdated) - self?.dismiss() - case .failure: - break - } - } - } - - @objc func keyboardWillChanged(noti: Notification) { - guard let userInfo = noti.userInfo else { return } - //let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval - let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue - - if endFrame.origin.y >= UIScreen.main.bounds.height { - bottomKeyboardHeight = 0 - refreshLPositon() - }else{ - bottomKeyboardHeight = endFrame.height - refreshLPositon() - } - } - -} diff --git a/crush/Crush/Src/Components/UI/Tags/RelationshipTag.swift b/crush/Crush/Src/Components/UI/Tags/RelationshipTag.swift deleted file mode 100644 index 69c9f56..0000000 --- a/crush/Crush/Src/Components/UI/Tags/RelationshipTag.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// RelationshipTag.swift -// Crush -// -// Created by Leon on 2025/8/13. -// - -import UIKit - -enum RelationshipType{ - case meet - case friend - case flirting - case couple - case married - - var localizedText: String { - switch self { - case .meet: - return "Meet" - case .friend: - return "Friend" - case .flirting: - return "Flirting" - case .couple: - return "Couple" - case .married: - return "Married" - } - } -} - -enum RelationshipTagSize{ - case small - case medium - case large -} - -class RelationshipTag: UIView { - var effectView: UIVisualEffectView! - var label: UILabel! - var gradientBg:GradientView! - - // MARK: Config - var heartBeatVal: CGFloat = 0 - var type: RelationshipType = .meet { - didSet { - setupData() - } - } - var tagSize: RelationshipTagSize = .medium - var tagHeight = 24.0 - var tagFont = UIFont.t.tls - var lrPadding = 4.0 - var isShowRelation: Bool = false - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - init(type:RelationshipType = .meet, size:RelationshipTagSize = .medium){ - super.init(frame: .zero) - self.type = type - tagSize = size - - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - layer.cornerRadius = 4 - layer.masksToBounds = true - - //❓是否需要,因为底部可能是有透明 - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 0.5 - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - switch tagSize { - case .small: - tagHeight = 16 - tagFont = .t.tls - lrPadding = 2.0 - case .medium: - tagHeight = 24 - tagFont = .t.tls - lrPadding = 4.0 - case .large: - tagHeight = 32 - tagFont = .t.tlm - lrPadding = 8.0 - } - - snp.makeConstraints { make in - make.height.equalTo(tagHeight) - } - - gradientBg = { - let gradient = CLSystemToken.gradient(token: .cpgn) - let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - v.isHidden = true - return v - }() - - label = { - let v = UILabel() - v.font = tagFont - v.textColor = .c.ctpn - v.setContentCompressionResistancePriority(UILayoutPriority(756), for: .horizontal) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(lrPadding) - make.trailing.equalToSuperview().offset(-lrPadding) - make.centerY.equalToSuperview() - } - return v - }() - } - - private func setupData(){ - - } - - private func setupEvent(){ - - } - - // MARK: - Helper - private func refreshViewsShow(){ - gradientBg.isHidden = true - - label.text = getShowStringBy(type.localizedText) - - switch type { - case .meet: - backgroundColor = .c.csen - case .friend: - backgroundColor = CLGlobalToken.color(token: .glo_color_violet_40)?.withAlphaComponent(0.45) - case .flirting: - backgroundColor = CLGlobalToken.color(token: .glo_color_orange_50)?.withAlphaComponent(0.45) - case .couple: - backgroundColor = CLGlobalToken.color(token: .glo_color_magenta_50)?.withAlphaComponent(0.45) - case .married: - backgroundColor = .clear - gradientBg.isHidden = false - } - } - - private func getShowStringBy(_ text: String) -> String{ - let val = heartBeatVal - - let formatVal = val.formatted(decimal: 1, usesGroupingSeparator: false) - if val > 0 && isShowRelation{ - let array = [text, "\(formatVal)℃"] - return array.joined(separator: " · ") - }else if val > 0{ - return "\(formatVal)℃" - } - else{ - return text - } - } - - // MARK: - Public - func bind(level: HeartbeatLevel?, heartBeatVal: CGFloat? = 0, isShow: Bool? = false){ - guard let heartLevel = level else{ - isHidden = true - return - } - isHidden = false - self.isShowRelation = isShow ?? false - - self.heartBeatVal = heartBeatVal ?? 0.0 - - type = heartLevel.relationType - - refreshViewsShow() - } -} diff --git a/crush/Crush/Src/Components/UI/Tags/RoleTags.swift b/crush/Crush/Src/Components/UI/Tags/RoleTags.swift deleted file mode 100644 index ba2f3cc..0000000 --- a/crush/Crush/Src/Components/UI/Tags/RoleTags.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// TagLabel.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -enum RoleTagStyle{ - case `default` - case purple - case theme - case blurPurple - case blurTheme -} - -/// 角色的Character Code、设定展示用 -class RoleTag: UIView{ - - var label:UILabel! - - var title: String = ""{ - didSet{ - refreshViews() - } - } - - var style: RoleTagStyle = .purple{ - didSet{ - refreshViews() - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews(){ - layer.cornerRadius = 4 - layer.masksToBounds = true - - self.snp.makeConstraints { make in - make.height.equalTo(24) - } - - label = { - let view = UILabel() - view.font = .t.tls - view.textColor = .c.ctpn - addSubview(view) - view.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(6) - make.trailing.equalToSuperview().offset(-6) - } - return view - }() - } - - private func refreshViews(){ - label.text = title - switch style{ - case .default: - backgroundColor = .c.csen - case .purple: - backgroundColor = UIColor(red: 0.48, green: 0.28, blue: 1, alpha: 1) - case .theme: - backgroundColor = UIColor(red: 0.82, green: 0.12, blue: 0.47, alpha: 1) - case .blurPurple: - backgroundColor = UIColor(red: 0.48, green: 0.28, blue: 1, alpha: 0.45) - case .blurTheme: - backgroundColor = UIColor(red: 0.82, green: 0.12, blue: 0.47, alpha: 0.45) - } - } - - -} diff --git a/crush/Crush/Src/Components/UI/TextViews/CLSelectView.swift b/crush/Crush/Src/Components/UI/TextViews/CLSelectView.swift deleted file mode 100644 index 4501e1e..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/CLSelectView.swift +++ /dev/null @@ -1,258 +0,0 @@ -// -// CLSelectView.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -import UIKit -import SnapKit - -class CLSelectView: UIView { - // MARK: - Properties - var titleLabel: UILabel - var arrowImageView: UIImageView - var backButton: UIButton - - var placeholder: String? { - didSet { - setupTheme() - if contentStr == nil || contentStr!.isEmpty { - contentLabel.text = placeholder - setupTheme() - } - } - } - - var placeholderColor: UIColor? { - didSet { - setupTheme() - } - } - - var forceSupportLabelColor: UIColor? { - didSet { - setupTheme() - } - } - - var contentStr: String? { - didSet { - setupTheme() - if let contentStr = contentStr, !contentStr.isEmpty { - contentLabel.text = contentStr - setupTheme() - if enableClearMode { - closeButton.isHidden = false - arrowImageView.isHidden = true - } - } else { - let place = self.placeholder - placeholder = place - if enableClearMode { - closeButton.isHidden = true - arrowImageView.isHidden = false - } - } - } - } - - var enableClearMode: Bool = false - - var isEnabled: Bool = true { - didSet { - isUserInteractionEnabled = isEnabled - arrowImageView.isHidden = false - if isEnabled { - titleLabel.textColor = .c.ctpn - backButton.backgroundColor = .c.csen - contentLabel.textColor = .c.ctpn - arrowImageView.image = MWIconFont.image(fromIcon: .arrowDownFill, size: CGSize(width: 16, height: 16), color: UIColor.c.ctsn, edgeInsets: .zero) - } else { - titleLabel.textColor = .c.ctd - backButton.backgroundColor = .c.csed - contentLabel.textColor = .c.ctd - arrowImageView.isHidden = true - //arrowImageView.image = MWIconFont.image(fromIcon: .arrowDownFill, size: CGSize(width: 16, height: 16), color: .c.ctsd, edgeInsets: .zero) - } - setupTheme() - } - } - - var selectBlock: (() -> Void)? - var clearBlock: (() -> Void)? - - private var stackV: UIStackView - private var queryButton: EPIconTertiaryButton? - private var contentStackH: UIStackView - private var contentLabel: UILabel - private var supportLabel: UILabel - private var closeButton: EPIconGhostSecondaryButton! - private var tapQueryButton: (() -> Void)? - - // MARK: - Initialization - override init(frame: CGRect) { - titleLabel = UILabel() - arrowImageView = UIImageView() - backButton = UIButton(type: .custom) - stackV = UIStackView() - contentStackH = UIStackView() - contentLabel = UILabel() - supportLabel = UILabel() - - super.init(frame: frame) - initialViews() - isEnabled = true - } - - override func awakeFromNib() { - super.awakeFromNib() - initialViews() - isEnabled = true - setupTheme() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - private func initialViews() { - let heightOfBackButton: CGFloat = 48 - let titleBetweenBackButtonPadding = 8//EPGlobalToken.spacing(.space12) - - // Configure stack view - stackV.axis = .vertical - stackV.spacing = 4 - addSubview(stackV) - stackV.snp.makeConstraints { make in - make.bottom.leading.trailing.equalTo(self) - } - - // Configure back button - stackV.addArrangedSubview(backButton) - backButton.layer.cornerRadius = 8 - backButton.backgroundColor = .c.csen - backButton.snp.makeConstraints { make in - make.height.equalTo(heightOfBackButton) - } - backButton.addTarget(self, action: #selector(backButtonAction), for: .touchUpInside) - - // Configure content stack - contentStackH.isUserInteractionEnabled = false - contentStackH.spacing = 8 - contentStackH.alignment = .center - addSubview(contentStackH) - contentStackH.snp.makeConstraints { make in - make.left.equalTo(backButton).offset(16) - make.right.lessThanOrEqualTo(self).offset(-24) - make.centerY.equalTo(backButton) - } - - // Configure content label - contentLabel.font = CLSystemToken.font(token: .tbl)//EPSystemToken.typography(.txtBodyL).font - contentStackH.addArrangedSubview(contentLabel) - contentLabel.setContentCompressionResistancePriority(.init(749), for: .horizontal) - - // Configure title label - titleLabel.font = CLSystemToken.font(token: .tlm) - titleLabel.textColor = .c.ctpn - addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.left.equalTo(self) - make.bottom.equalTo(backButton.snp.top).offset(-titleBetweenBackButtonPadding) - make.top.equalTo(self).offset(0) - } - - // Configure arrow image view - arrowImageView.isHidden = false - arrowImageView.image = MWIconFont.image(fromIcon: .arrowDownFill, size: CGSize(width: 16, height: 16), color: .c.ctsn, edgeInsets: .zero) - addSubview(arrowImageView) - arrowImageView.snp.makeConstraints { make in - make.centerY.equalTo(backButton) - make.right.equalTo(self).offset(-16) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - - // Configure support label - supportLabel.font = CLSystemToken.font(token: .tbs) - supportLabel.numberOfLines = 0 - supportLabel.textColor = .c.ctsn - - stackV.addArrangedSubview(supportLabel) - - // Default placeholder color - placeholderColor = .c.cttn//EPSystemToken.color(.txtTertiaryNormal) - - setupTheme() - } - - private func setupTheme() { - if isEnabled{ - contentLabel.textColor = (contentStr?.isEmpty ?? true) ? placeholderColor : .c.ctpn - supportLabel.textColor = forceSupportLabelColor ?? .c.civn - backButton.layer.borderColor = UIColor.c.civn.cgColor - }else{ - contentLabel.textColor = .c.ctpd - supportLabel.textColor = .c.ctsd - } - - } - - // MARK: - Actions - @objc private func backButtonAction() { - selectBlock?() - } - - @objc private func tapCloseButton() { - clearBlock?() - } - - @objc private func tapQueryBtn() { - tapQueryButton?() - } - - // MARK: - Public Methods - func setupSelect(canSelect: Bool, selectBlock: @escaping (String?) -> Void) { - self.isEnabled = canSelect - // Note: Original code doesn't use the selectBlock parameter; assuming it's a mistake - // If needed, store selectBlock for use in backButtonAction - } - - func showErrorMsg(_ string: String) { - supportLabel.isHidden = false - backButton.layer.borderWidth = CLSystemToken.border(token: .bs) - //EPSystemToken.border(.borderS) - supportLabel.text = string - } - - func hideErrorInfo() { - supportLabel.isHidden = true - backButton.layer.borderWidth = 0 - } - - func showForceNormalSupportMsg(_ string: String) { - guard !string.isEmpty else { return } - forceSupportLabelColor = .c.ctsn - supportLabel.isHidden = false - supportLabel.text = string - supportLabel.textColor = .c.ctsn - } - - func titleAppendQuestionIcon(_ block: @escaping () -> Void) { - if queryButton == nil { - tapQueryButton = block - queryButton = EPIconTertiaryButton(radius: .round, iconSize: .xxs, iconCode: .question) - queryButton?.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - queryButton?.addTarget(self, action: #selector(tapQueryBtn), for: .touchUpInside) - addSubview(queryButton!) - queryButton?.snp.makeConstraints { make in - make.leading.equalTo(titleLabel.snp.trailing).offset(8) - make.centerY.equalTo(titleLabel) - make.trailing.lessThanOrEqualTo(self).offset(-8) - } - } - queryButton?.isHidden = false - } -} diff --git a/crush/Crush/Src/Components/UI/TextViews/CLTextField.swift b/crush/Crush/Src/Components/UI/TextViews/CLTextField.swift deleted file mode 100644 index 245d179..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/CLTextField.swift +++ /dev/null @@ -1,351 +0,0 @@ -// -// CLTextField.swift -// Crush -// -// Created by Leon on 2025/7/15. -// - -import UIKit - -class CLTextField: UITextField { - // MARK: - Properties - private var focusBorderColor: UIColor = .orange - private var errorBorderColor: UIColor = .red - private var nowBorderColor: UIColor = .clear - - private var tapTopButtonAction: (() -> Void)? - - /// 在此模式下,errorBorder 在新修改内容之前都会显示 border。 - private var errorBorderShowMode: Bool = false - - private var alwaysShowRightViewConfig: Bool = false - private var alwaysShowClearButtonIfHave: Bool = false - private var disableAllBorder: Bool = false - private var fakeTextFieldForInputTrigger: Bool = false - - var cornerRadiusConfig:CGFloat = 8 - - // private var defaultTxt: String? - - // Closures for delegate-like events (assumed from Objective-C) - var textDidChanged: ((CLTextField) -> Void)? - var textDidBeginEditing: ((CLTextField) -> Void)? - var textDidEndEditing: ((CLTextField) -> Void)? - var textShouldBeginEditing: ((CLTextField) -> Bool)? - - // MARK: - Initialization - override init(frame: CGRect) { - super.init(frame: frame) - baseCommonSetup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - baseCommonSetup() - } - - private func baseCommonSetup() { - // keyboardAppearance = .dark - // reloadInputViews() - setContentHuggingPriority(UILayoutPriority(244), for: .horizontal) - - - let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) - leftView.backgroundColor = .clear - self.leftViewMode = .always - self.leftView = leftView - - // --- 3 events - addTarget(self, action: #selector(textDidChanged(_:)), for: .editingChanged) - NotificationCenter.default.addObserver(self, selector: #selector(notiTextDidBeginEditing), name: UITextField.textDidBeginEditingNotification, object: self) - NotificationCenter.default.addObserver(self, selector: #selector(notiTextDidEndEditing), name: UITextField.textDidEndEditingNotification, object: self) - NotificationCenter.default.addObserver(self, selector: #selector(notiTextShouldBeginEditing(_:)), name: UITextField.textDidBeginEditingNotification, object: self) - - setupStandard() - } - - // MARK: - Public Methods - func setupStandard() { - autocorrectionType = .no - backgroundColor = .c.csen - font = CLSystemToken.font(token: .tbl) - textColor = .white - - focusBorderColor = .theme - errorBorderColor = .red - layer.borderWidth = 1 - layer.borderColor = UIColor.clear.cgColor - - layer.cornerRadius = cornerRadiusConfig - -// NSLayoutConstraint.activate([ -// heightAnchor.constraint(equalToConstant: 48) -// ]) - - snp.makeConstraints { make in - make.height.equalTo(48) - } - - // default - setupRightClearButton() - } - - func setupPlaceholder(_ placeholder: String) { - let font = UIFont.systemFont(ofSize: 15) - let color = UIColor.gray - let attributes: [NSAttributedString.Key: Any] = [ - .font: font, - .foregroundColor: color - ] - attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes) - } - - func setupTopButton(with action: (() -> Void)?) { - let topButton = UIButton(type: .custom) - topButton.backgroundColor = .clear - addSubview(topButton) - topButton.addTarget(self, action: #selector(tapTopButton), for: .touchUpInside) - tapTopButtonAction = action - - // Using SnapKit or native Auto Layout - topButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - topButton.topAnchor.constraint(equalTo: topAnchor), - topButton.bottomAnchor.constraint(equalTo: bottomAnchor), - topButton.leadingAnchor.constraint(equalTo: leadingAnchor), - topButton.trailingAnchor.constraint(equalTo: trailingAnchor) - ]) - } - - func showAlwayErrorBorder() { - errorBorderShowMode = true - layer.borderWidth = 1 - layer.borderColor = errorBorderColor.cgColor - } - - func hideErrorBorder() { - errorBorderShowMode = false - layer.borderColor = UIColor.clear.cgColor - } - - func sendTextChangedNoti(){ - NotificationCenter.default.post( - name: UITextField.textDidChangeNotification, - object: self) - } - - // MARK: - Helper Methods - func setupRightClearButton() { - rightViewMode = .whileEditing - let rightView = UIView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) - self.rightView = rightView - - let clearButton = EPIconGhostSecondaryButton(radius: .none, iconSize: .small, iconCode: .delete02) - rightView.addSubview(clearButton) - clearButton.addTarget(self, action: #selector(tapClearButton), for: .touchUpInside) - clearButton.snp.makeConstraints { make in - make.size.equalTo(clearButton.bgImageSize()) - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-8) - } - } - - func setupRightEyeButton() { - isSecureTextEntry = true - rightViewMode = .always - alwaysShowRightViewConfig = true - - let rightView = UIView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) - self.rightView = rightView - } - - func setupRightRightArrowButton() -> UIButton? { - rightViewMode = .always - alwaysShowRightViewConfig = true - - let rightView = UIView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) - self.rightView = rightView - return nil - } - - func setupRightViewEmpty() { - rightView = nil - } - - func setupLeftCoinIconView() { - leftViewMode = .always - let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 44, height: 40)) - let iconView = UIImageView(frame: CGRect(x: 16, y: 12, width: 16, height: 16)) - iconView.image = UIImage(named: "icon_16_diamond") - leftView.addSubview(iconView) - self.leftView = leftView - } - - func setupLeftBlankView() { - leftViewMode = .always - let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 40)) - self.leftView = leftView - } - - func adjustRightViewMode() { - if alwaysShowRightViewConfig { - rightViewMode = .always - return - } - - // 空数据不显示删除按钮 - if text?.isEmpty ?? true { - rightViewMode = .never - } else { - rightViewMode = alwaysShowClearButtonIfHave ? .always : .whileEditing - } - } - - // MARK: - Actions - @objc private func tapTopButton() { - tapTopButtonAction?() - } - - @objc private func tapClearButton() { - text = "" - textDidChanged(self) - NotificationCenter.default.post( - name: UITextField.textDidChangeNotification, - object: self) - } - - @objc private func tapRightArrowButton(_ button: UIButton) { - // Empty implementation as in original - } - - // MARK: - Textfield Events - @objc private func textDidChanged(_ textField: UITextField) { - errorBorderShowMode = false - nowBorderColor = focusBorderColor - - if !disableAllBorder && textField.isFirstResponder { - layer.borderColor = nowBorderColor.cgColor - } - - adjustRightViewMode() - - textDidChanged?(self) - } - - @objc private func notiTextDidBeginEditing() { - if !errorBorderShowMode && !disableAllBorder { - layer.borderColor = focusBorderColor.cgColor - layer.borderWidth = 1 - } - - textDidBeginEditing?(self) - - adjustRightViewMode() - } - - @objc private func notiTextDidEndEditing() { - if !errorBorderShowMode { - layer.borderColor = nil - layer.borderWidth = 0 - } - - textDidEndEditing?(self) - } - - @objc private func notiTextShouldBeginEditing(_ notification: Notification) { - if fakeTextFieldForInputTrigger { - resignFirstResponder() - } - - textShouldBeginEditing?(self) - } - - // MARK: - Setters - override var delegate: UITextFieldDelegate? { - get { super.delegate } - set { - if newValue !== self { - // print("请用头文件中的block回调") // Uncomment if assertion is needed - } - super.delegate = newValue - } - } - - override var placeholder: String? { - didSet { - if let placeholder = placeholder { - setupPlaceholder(placeholder) - } - } - } - -// var defaultTxt: String? { -// get { _defaultTxt } -// set { -// _defaultTxt = newValue -// super.text = newValue -// } -// } - - override var text: String? { - didSet { - if text != oldValue { - textDidChanged(self) - } - } - } - - // MARK: - Layout - override func rightViewRect(forBounds bounds: CGRect) -> CGRect { - var rightViewRect = super.rightViewRect(forBounds: bounds) - - if #available(iOS 13.0, *) { - // fix iOS 13 rightView显示在最左边,和异常显示问题 - let left = frame.size.width - (rightView?.frame.size.width ?? 0) - let top = round((frame.size.height - (rightView?.frame.size.height ?? 0)) / 2.0) - rightViewRect = CGRect(x: left, y: top, width: rightView?.frame.size.width ?? 0, height: rightView?.frame.size.height ?? 0) - } - - return rightViewRect - } - - override func layoutSubviews() { - super.layoutSubviews() - layer.cornerRadius = self.cornerRadiusConfig - } - - deinit { - NotificationCenter.default.removeObserver(self) - } -} - -extension CLTextField{ - func switchToNaviSearchField(){ - setupPlaceholder("Search") - font = .t.tbm - cornerRadiusConfig = 16// height: 32 - snp.updateConstraints { make in - make.height.equalTo(32) - } - - leftViewMode = .always - let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 32)) - leftView.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 40, height: 32)) - } - self.leftView = leftView - - let image = MWIconFont.image(fromIcon: .search, size: CGSize(width: 12, height: 12), color: .white) - let icon = UIImageView(image: image) - leftView.addSubview(icon) - icon.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 12, height: 12)) - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(12) - } - - setNeedsLayout() - layoutIfNeeded() - } - -} diff --git a/crush/Crush/Src/Components/UI/TextViews/CLTextView.swift b/crush/Crush/Src/Components/UI/TextViews/CLTextView.swift deleted file mode 100644 index e9721e1..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/CLTextView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// CLTextView.swift -// Crush -// -// Created by Leon on 2025/7/15. -// - -import Foundation -import IQTextView - -class CLTextView: IQTextView{ - // MARK: - Properties - private var recordHeightBlock: ((CGRect) -> Void)? - - // MARK: - Public Methods - func updateByPlaceholderSize(_ block: @escaping (CGRect) -> Void) { - recordHeightBlock = block - } - - func sendTextChangedNoti(){ - NotificationCenter.default.post( - name: UITextView.textDidChangeNotification, - object: self) - } - - // MARK: - Overrides - override func layoutSubviews() { - super.layoutSubviews() - if let recordHeightBlock = recordHeightBlock { - let realFrame = placeholderLabel.frame - let placeholderShowRect = CGRect(x: realFrame.origin.x, y: realFrame.origin.y, width: realFrame.size.width, height: realFrame.size.height + realFrame.origin.y) - recordHeightBlock(placeholderShowRect) - } - } - -// override func paste(_ sender: Any?) { -// if let pasteboardString = UIPasteboard.general.string?.filterGarbledString { -// let nowString = "\(text ?? "")\(pasteboardString)" -// text = nowString -// delegate?.textViewDidChange?(self) -// -// } -// } -} - -// MARK: - Extensions -extension String { - var filterGarbledString: String? { - // Assuming filterGarbledString removes invalid characters; implement as needed - return self - } -} diff --git a/crush/Crush/Src/Components/UI/TextViews/EGInputLimit.h b/crush/Crush/Src/Components/UI/TextViews/EGInputLimit.h deleted file mode 100644 index 40aafc9..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/EGInputLimit.h +++ /dev/null @@ -1,67 +0,0 @@ -// -// EGInputLimit.h -// EGCommon -// -// Created by donglyu on 2020/4/20. -// Copyright © 2020 Company. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface EGInputLimit : NSObject - -// 是否禁用 -@property (nonatomic, assign) BOOL disable; -/// 是否禁止输入emoji -@property (nonatomic, assign) BOOL disableEmoji; - -/// 限制最大可输入字符个数(length长度大于1的,按照1个字符计算) -@property (nonatomic, assign) NSInteger maxCharacterNumber; -/// 获取当前已经输入字符的个数 -@property (nonatomic, assign, readonly) NSInteger currentCharNumber; -/// 获取当前还可以输入字符的个数 -@property (nonatomic, assign, readonly) NSInteger canEnterCharNumber; - -/// 仅数字 -@property (nonatomic, assign) BOOL onlyNumbers; -/// 仅中文 -@property (nonatomic, assign) BOOL onlyChinese; -/// 仅英文字母 -@property (nonatomic, assign) BOOL onlyLetter; - -/// 限制小数输入 -@property (nonatomic, assign) BOOL decimaStyle; -/// 控制小数输入位数(当小于1时,保留1位) -@property (nonatomic, assign) NSInteger decimalPlace; - -/// 按照给定的正则式过滤 -@property (nonatomic, copy) NSString *filterPattern; -/// 按照给定的正则式输入 -@property (nonatomic, copy) NSString *allowPattern; - - -- (instancetype)initWithTextField:(UITextField *)textField; -- (instancetype)initWithTextView:(UITextView *)textView; - -+ (instancetype)new NS_UNAVAILABLE; -- (instancetype)init NS_UNAVAILABLE; - - - -@end - -NS_ASSUME_NONNULL_END - -NS_ASSUME_NONNULL_BEGIN - -@interface UITextView (WSInputLimit) -@property (nonatomic, strong, readonly) EGInputLimit *limit; -@end - -@interface UITextField (WSInputLimit) -@property (nonatomic, strong, readonly) EGInputLimit *limit; -@end - -NS_ASSUME_NONNULL_END diff --git a/crush/Crush/Src/Components/UI/TextViews/EGInputLimit.m b/crush/Crush/Src/Components/UI/TextViews/EGInputLimit.m deleted file mode 100644 index 9bef669..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/EGInputLimit.m +++ /dev/null @@ -1,197 +0,0 @@ -// -// EGInputLimit.m -// EGCommon -// -// Created by donglyu on 2020/4/20. -// Copyright © 2020 Company. All rights reserved. -// - -#import "EGInputLimit.h" -#import - -NSString *const WSInputLimitOnlyNumbersPattern = @"[0-9]+"; -NSString *const WSInputLimitOnlyChinesePattern = @"[\u4e00-\u9fa5]+"; -NSString *const WSInputLimitOnlyLetterPattern = @"[A-Za-z]+"; -/// 处理emoji符号,数值、各语言字母不会处理 -NSString *const WSInputLimitFilterEmojiPattern = @"[\U0001f0a0-\U0001f0ff\U0001f170-\U0001f19a\U0001f300-\U0001faff\u2190-\u21FF\u2300-\u23FF\u2500-\u26FF\u2700-\u27BF\u27F0-\u27FF\u2900-\u297F\u2B00-\u2BFF]+"; -// 下方是旧正则、部分其他语言的文案不通过,eg: évaluation -//@"[^\\u0020-\\u007E\\u00A0-\\u00BE\\u2E80-\\uA4CF\\uF900-\\uFAFF\\uFE30-\\uFE4F\\uFF00-\\uFFEF\\u0080-\\u009F\\u2000-\\u201f\r\n]"; - -static inline NSString *WSInputLimitDecimaStylePattern(NSInteger decimalPlace) { - return [NSString stringWithFormat:@"([1-9][0-9]*|0)(\\.)?([0-9]{1,%ld})?", MAX(1, decimalPlace)]; -} - - -@interface EGInputLimit() -@property (nonatomic, weak, readonly) UITextField *textField; -@property (nonatomic, weak, readonly) UITextView *textView; -@end - -@implementation EGInputLimit - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (instancetype)initWithTextField:(UITextField *)textField { - if (self = [super init]) { - _textField = textField; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldTextDidChange:) name:UITextFieldTextDidChangeNotification object:_textField]; - } - return self; -} - -- (instancetype)initWithTextView:(UITextView *)textView { - if (self = [super init]) { - _textView = textView; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textViewTextDidChange:) name:UITextViewTextDidChangeNotification object:_textView]; - } - return self; -} - -#pragma mark textFieldTextDidChange -- (void)textFieldTextDidChange:(NSNotification *)notifi { - if (_disable) { - return; - } - UITextField *textField = notifi.object; - if (textField == _textField) { - UITextRange *markedTextRange = [_textField markedTextRange]; - UITextPosition *position = [_textField positionFromPosition:markedTextRange.start offset:0]; - if (markedTextRange && position) { - return; - } - - NSString *result = [self handleText:_textField.text]; - UITextRange* selectedRange = _textField.selectedTextRange; - _textField.text = result; - _textField.selectedTextRange = selectedRange; - } -} - -#pragma mark textViewTextDidChange -- (void)textViewTextDidChange:(NSNotification *)notifi { - if (_disable) { - return; - } - UITextView *textView = notifi.object; - if (textView == _textView) { - UITextRange *markedTextRange = [_textView markedTextRange]; - UITextPosition *position = [_textView positionFromPosition:markedTextRange.start offset:0]; - if (markedTextRange && position) { - return; - } - - NSString *result = [self handleText:_textView.text]; - NSRange selectedRange = _textView.selectedRange; - _textView.text = result; - _textView.selectedRange = NSMakeRange(selectedRange.location, 0); - } -} - -- (NSString *)handleText:(NSString *)text { - if (_onlyNumbers) { - NSString *result = [self subStringFromText:text withPattern:WSInputLimitOnlyNumbersPattern]; - return result; - } - - if (_onlyChinese) { - NSString *result = [self subStringFromText:text withPattern:WSInputLimitOnlyChinesePattern]; - return result; - } - - if (_onlyLetter) { - NSString *result = [self subStringFromText:text withPattern:WSInputLimitOnlyLetterPattern]; - return result; - } - - if (_decimaStyle) { - NSString *pattern = WSInputLimitDecimaStylePattern(_decimalPlace); - NSString *result = [self subStringFromText:text withPattern:pattern]; - return result; - } - - if (_allowPattern) { - NSString *result = [self subStringFromText:text withPattern:_allowPattern]; - return result; - } - - NSString *filterPattern = _filterPattern; - if (_disableEmoji) { - filterPattern = WSInputLimitFilterEmojiPattern; - } - if (filterPattern) { - NSRegularExpression *regex = [[NSRegularExpression alloc]initWithPattern:filterPattern options:0 error:NULL]; - NSString *result = [regex stringByReplacingMatchesInString:text - options:0 - range:NSMakeRange(0, text.length) - withTemplate:@""]; - result = [self lengthHandle:result]; - return result; - } - - NSString *result = [self lengthHandle:text]; - return result; -} - -- (NSString *)subStringFromText:(NSString *)string withPattern:(NSString *)pattern { - NSRange range = [string rangeOfString:pattern - options:NSRegularExpressionSearch - range:NSMakeRange(0, string.length)]; - NSString *result = @""; - if (range.location != NSNotFound) { - result = [string substringWithRange:range]; - } - result = [self lengthHandle:result]; - return result; -} - -- (NSString *)lengthHandle:(NSString *)text { - __block NSString *filterText = @""; - __block NSInteger currentChatNum = 0; - NSInteger maxCharacterNumber = _maxCharacterNumber; - [text enumerateSubstringsInRange:NSMakeRange(0, text.length) - options:NSStringEnumerationByComposedCharacterSequences - usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) { - if (maxCharacterNumber > 0 && currentChatNum >= maxCharacterNumber) { - *stop = YES; - } else { - filterText = [filterText stringByAppendingString:substring]; - currentChatNum ++; - } - }]; - _currentCharNumber = currentChatNum; - _canEnterCharNumber = MAX(0, _maxCharacterNumber - _currentCharNumber); - return filterText; -} - -@end - -@implementation UITextView (WSInputLimit) - -- (EGInputLimit *)limit { - EGInputLimit *limit = objc_getAssociatedObject(self, _cmd); - if (!limit) { - limit = [[EGInputLimit alloc]initWithTextView:self]; - objc_setAssociatedObject(self, _cmd, limit, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return limit; -} - -@end - -@implementation UITextField (WSInputLimit) - -- (EGInputLimit *)limit { - EGInputLimit *limit = objc_getAssociatedObject(self, _cmd); - if (!limit) { - limit = [[EGInputLimit alloc]initWithTextField:self]; - objc_setAssociatedObject(self, _cmd, limit, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return limit; -} - -@end diff --git a/crush/Crush/Src/Components/UI/TextViews/NumbersInputBox.swift b/crush/Crush/Src/Components/UI/TextViews/NumbersInputBox.swift deleted file mode 100644 index 5f717e3..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/NumbersInputBox.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// NumbersInputBox.swift -// Crush -// -// Created by Leon on 2025/7/23. -// - -class NumbersInputBox: CLTextField { - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - - private func setup() { - font = .t.tnmm - textColor = .c.ctpn - - - // 设置固定尺寸 -// frame = CGRect(x: 0, y: 0, width: 80, height: 48) - snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 80, height: 48)) - } - - // 设置内容居中 - textAlignment = .center - - // 设置数字键盘 - keyboardType = .numberPad - - // 设置背景和边框 - backgroundColor = .c.csen - layer.cornerRadius = 8 -// layer.borderWidth = 1 -// layer.borderColor = UIColor.gray.cgColor -// layer.shadowRadius = 2 -// layer.shadowOpacity = 0.2 -// layer.shadowOffset = CGSize(width: 0, height: 1) - - // 设置内边距 - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: frame.height)) - leftView = paddingView - leftViewMode = .always - rightView = paddingView - rightViewMode = .always - } -} diff --git a/crush/Crush/Src/Components/UI/TextViews/TitleTextField.swift b/crush/Crush/Src/Components/UI/TextViews/TitleTextField.swift deleted file mode 100644 index bca9934..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/TitleTextField.swift +++ /dev/null @@ -1,378 +0,0 @@ -// -// TitleTextField.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -import SnapKit -import UIKit - -class TitleTextField: UIView { - // MARK: - Properties - - var titleLabel: UILabel - var textfield: CLTextField - var supportLabel: UILabel - - var placeholder: String? { - didSet { - textfield.placeholder = placeholder - } - } - var errorMsg: String? - var minLimit: Int = 0 - var maxLimit: Int = 0 { - didSet{ - textfield.limit.maxCharacterNumber = maxLimit - } - } - - private var stackV: UIStackView - private var tapQueryBlock: (() -> Void)? - - // MARK: - Initialization - - override init(frame: CGRect) { - titleLabel = UILabel() - textfield = CLTextField() - supportLabel = UILabel() - stackV = UIStackView() - - super.init(frame: frame) - - initialViews() - setupEvent() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - - private func initialViews() { - // Configure stack view - stackV.axis = .vertical - stackV.alignment = .leading - stackV.spacing = 12 - addSubview(stackV) - stackV.snp.makeConstraints { make in - make.top.bottom.leading.trailing.equalTo(self) - } - - // Configure title label - titleLabel.font = CLSystemToken.typography(token: .tlm).font! // EPSystemToken.typography(.txtLabelM).font - titleLabel.textColor = .c.ctpn - - // Configure text field - - - // Configure support label - supportLabel.numberOfLines = 0 - supportLabel.font = CLSystemToken.typography(token: .tbs).font! - supportLabel.textColor = .c.ctsn - - // Default state - supportLabel.isHidden = true - - // Add subviews to stack - stackV.addArrangedSubview(titleLabel) - stackV.addArrangedSubview(textfield) - stackV.addArrangedSubview(supportLabel) - - // Additional constraints - titleLabel.snp.makeConstraints { make in - make.right.lessThanOrEqualTo(stackV) - } - - textfield.snp.makeConstraints { make in - make.leading.trailing.equalTo(stackV) - } - } - - private func setupEvent() { - // No events in the original, kept as a placeholder - - textfield.addTarget(self, action: #selector(textDidChanged(_:)), for: .editingChanged) - } - - // MARK: - Public Methods - - func showNormalSupportMsg(_ string: String) { - supportLabel.isHidden = false - supportLabel.textColor = .c.ctsn // EPSystemToken.tsn - supportLabel.text = string - } - - func showWarningMsg(_ string: String) { - supportLabel.isHidden = false - supportLabel.textColor = .c.cwvn // EPSystemToken.wvn - supportLabel.text = string - } - - func showErrorMsg(_ string: String) { - supportLabel.isHidden = false - supportLabel.textColor = .c.civn // EPSystemToken.color(.importantVariantNormal) - supportLabel.text = string - textfield.showAlwayErrorBorder() - } - - func hideErrorInfo() { - supportLabel.isHidden = true - textfield.hideErrorBorder() - } - - func titleAppendOptionalLabel() { - // ... - } - - func setupQueryBtn(block: @escaping () -> Void) { - tapQueryBlock = block - let button = UIButton(type: .custom) - button.setImage(UIImage(named: "query_lightpurple"), for: .normal) - button.contentMode = .center - addSubview(button) - - let label = titleLabel - button.snp.makeConstraints { make in - make.centerY.equalTo(label) - make.left.equalTo(label.snp.right).offset(-4) - make.size.equalTo(CGSize(width: 44, height: 44)) - } - - button.addTarget(self, action: #selector(queryButtonTapped), for: .touchUpInside) - } - - func setupEmailMode() { - textfield.autocapitalizationType = .none - textfield.autocorrectionType = .no - textfield.keyboardType = .emailAddress - textfield.placeholder = NSLocalizedString("email", comment: "") - textfield.limit.maxCharacterNumber = 50 - } - - func setupPasswordMode() { - textfield.autocapitalizationType = .none - textfield.autocorrectionType = .no - textfield.isSecureTextEntry = true - textfield.keyboardType = .asciiCapable - textfield.placeholder = NSLocalizedString("password", comment: "") - textfield.limit.maxCharacterNumber = 15 - textfield.setupRightEyeButton() - clipsToBounds = true - } - - func verifyPsswordDataValid(isFinalStep: Bool) -> Bool { - let text = textfield.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - - if text.isEmpty { - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("hint_enter_password", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - if text.count < 6 || text.count > 15 { - if isFinalStep || !textfield.isFirstResponder || text.count > 15 { - showErrorMsg(NSLocalizedString("password_error_info", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - // Check for digits - let numPattern = ".*\\d+.*" - let numPred = NSPredicate(format: "SELF MATCHES %@", numPattern) - if !numPred.evaluate(with: text) { - print("Password must contain digits") - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("toast_password_need_6_15_characters", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - // Check for lowercase letters - let lowerPattern = ".*[a-z]+.*" - let lowerPred = NSPredicate(format: "SELF MATCHES %@", lowerPattern) - if !lowerPred.evaluate(with: text) { - print("Password must contain lowercase letters") - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("toast_password_need_6_15_characters", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - // Check for uppercase letters - let upperPattern = ".*[A-Z]+.*" - let upperPred = NSPredicate(format: "SELF MATCHES %@", upperPattern) - if !upperPred.evaluate(with: text) { - print("Password must contain uppercase letters") - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("toast_password_need_6_15_characters", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - hideErrorInfo() - return true - } - - func verifyOldPsswordDataValid(isFinalStep: Bool) -> Bool { - let text = textfield.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - - if text.isEmpty { - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("hint_enter_password", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - if text.count < 6 || text.count > 15 { - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("password_error_info", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - hideErrorInfo() - return true - } - - func verifyEmailDataValid(isFinalStep: Bool) -> Bool { - let text = textfield.text ?? "" - - if text.isEmpty { - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("hint_enter_email", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - if !text.isValidEmail { - if isFinalStep || !textfield.isFirstResponder { - showErrorMsg(NSLocalizedString("email_format_incorrect", comment: "")) - } else { - hideErrorInfo() - } - return false - } - - return true - } - - func verifyNewStylePassword(isFinalStep: Bool) -> Bool { - let text = textfield.text ?? "" - let textEmpty = text.isEmpty - - var condition1 = true - var condition2 = true - var condition3 = true - - // Check length - if text.count < 6 || text.count > 15 { - condition1 = false - } - - // Check for digits - let numPattern = ".*\\d+.*" - let numPred = NSPredicate(format: "SELF MATCHES %@", numPattern) - if !numPred.evaluate(with: text) { - print("Password must contain digits") - condition2 = false - } - - // Check for lowercase letters - let lowerPattern = ".*[a-z]+.*" - let lowerPred = NSPredicate(format: "SELF MATCHES %@", lowerPattern) - if !lowerPred.evaluate(with: text) { - print("Password must contain lowercase letters") - condition3 = false - } - - // Check for uppercase letters - let upperPattern = ".*[A-Z]+.*" - let upperPred = NSPredicate(format: "SELF MATCHES %@", upperPattern) - if !upperPred.evaluate(with: text) { - print("Password must contain uppercase letters") - condition3 = false - } - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 4 - - let normalAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: UIColor.c.ctsn, - .font: CLSystemToken.typography(token: .tbs).font!, - .paragraphStyle: paragraphStyle, - ] - - let condition1String = "• \(NSLocalizedString("password_count_greater_than_6", comment: ""))" - let condition2String = "• \(NSLocalizedString("password_contain_numbers", comment: ""))" - let condition3String = "• \(NSLocalizedString("password_contain_upper_lower_letters", comment: ""))" - let full = "\(condition1String)\n\(condition2String)\n\(condition3String)" - - let attributedString = NSMutableAttributedString(string: full, attributes: normalAttributes) - - if !textEmpty { - let greenAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.c.cpvn] // EPSystemToken.color(.positiveVariantNormal) - let redAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.c.civn] // EPSystemToken.color(.importantVariantNormal) - - let range1 = (full as NSString).range(of: condition1String) - let range2 = (full as NSString).range(of: condition2String) - let range3 = (full as NSString).range(of: condition3String) - - attributedString.addAttributes(condition1 ? greenAttributes : redAttributes, range: range1) - attributedString.addAttributes(condition2 ? greenAttributes : redAttributes, range: range2) - attributedString.addAttributes(condition3 ? greenAttributes : redAttributes, range: range3) - } - - supportLabel.isHidden = false - supportLabel.attributedText = attributedString - - return condition1 && condition2 && condition3 - } - - // MARK: - Actions - - @objc private func textDidChanged(_ textField: UITextField) { - - if (textField.text ?? "").count < minLimit{ - if let msg = errorMsg{ - showErrorMsg(msg) - } - }else{ - hideErrorInfo() - } - } - - @objc private func queryButtonTapped() { - tapQueryBlock?() - } - - // MARK: - Responder - - override func becomeFirstResponder() -> Bool { - textfield.becomeFirstResponder() - } - - override func resignFirstResponder() -> Bool { - textfield.resignFirstResponder() - } -} diff --git a/crush/Crush/Src/Components/UI/TextViews/TitleTextView.swift b/crush/Crush/Src/Components/UI/TextViews/TitleTextView.swift deleted file mode 100644 index 413ab4e..0000000 --- a/crush/Crush/Src/Components/UI/TextViews/TitleTextView.swift +++ /dev/null @@ -1,402 +0,0 @@ -// -// TitleTextView.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit -import SnapKit - -private let defaultHeightForTextView: CGFloat = 140.0 - -class TitleTextView: UIView, UITextViewDelegate { - // MARK: - Properties - var textDidChanged: ((CLTextView) -> Void)? - var textDidEndEditing: ((CLTextView) -> Void)? - var textDidBeginEditing: ((CLTextView) -> Void)? - var textShouldBeginEditing: ((CLTextView) -> Void)? - - var maxLimit: Int = 0 { - didSet { - if maxLimit > 0 { - countLabel.isHidden = false - countLabel.text = "0/\(maxLimit)" - textView.limit.maxCharacterNumber = maxLimit - textView.snp.updateConstraints { make in - make.bottom.equalTo(textContainer).offset(-48) - } - } - } - } - - var minLimit: Int = 0 - - var titleLabel: UILabel - var textView: CLTextView - var supportLabel: UILabel - - var placeholder: String? { - didSet { - textView.placeholder = placeholder - } - } - - /// 同时更新countLabel - var defaultText: String?{ - didSet{ - textView.text = defaultText - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[weak self] in - guard let self = self else{return} - self.subTextDidChange(self.textView) - } - - } - } - var errorMsg: String? - - /// 配置固定高度 - var constHeightOfTextView: CGFloat = 0 - - private var stackV: UIStackView - private var textContainer: UIView - private var countLabel: UILabel - private var optionLabel: EPOptionFlagLabel? - private var queryButton: EPIconTertiaryButton? - private var lastText: String? - private var location: Int = 0 - private var length: Int = 0 - private var focusBorderColor: UIColor - private var errorBorderColor: UIColor - private var nowBorderColor: UIColor - private var errorBorderShowMode: Bool = false - private var realDefaultHeightForTextView: CGFloat - private var tapQueryButton: (() -> Void)? - - // MARK: - Initialization - override init(frame: CGRect) { - titleLabel = UILabel() - textView = CLTextView() - supportLabel = UILabel() - stackV = UIStackView() - textContainer = UIView() - countLabel = UILabel() - focusBorderColor = .c.cpvn - errorBorderColor = .c.civn - nowBorderColor = .clear - realDefaultHeightForTextView = defaultHeightForTextView - - super.init(frame: frame) - initialViews() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup - private func initialViews() { - // Configure stack view - stackV.axis = .vertical - stackV.alignment = .leading - stackV.spacing = 16 - addSubview(stackV) - stackV.snp.makeConstraints { make in - make.top.bottom.leading.trailing.equalTo(self) - } - - // Configure title label - titleLabel.font = CLSystemToken.font(token: .tlm)//EPSystemToken.typography(.txtLabelM).font - titleLabel.textColor = .c.ctpn - - // Configure text container - textContainer.backgroundColor = .c.csen - textContainer.layer.cornerRadius = CLSystemToken.radius(token: .rs) - textContainer.layer.borderWidth = CLSystemToken.border(token: .bs)//EPSystemToken.border(.borderS) - textContainer.layer.borderColor = UIColor.clear.cgColor - - // Configure text view - textView.delegate = self - textView.backgroundColor = .clear - textView.font = CLSystemToken.font(token: .tbl)//EPSystemToken.typography(.txtBodyL).font - textView.placeholderLabel.font = CLSystemToken.font(token: .tbl) - //textView.iqPlaceholderLabel?.font = EPSystemToken.typography(.txtBodyL).font - textView.placeholderTextColor = .c.cttn - textView.textColor = .c.ctpn - - textContainer.addSubview(textView) - textView.snp.makeConstraints { make in - make.top.equalTo(textContainer).offset(8) - make.leading.equalTo(textContainer).offset(16) - make.trailing.equalTo(textContainer).offset(-16) - make.bottom.equalTo(textContainer).offset(-16) - make.height.greaterThanOrEqualTo(realDefaultHeightForTextView) - make.height.equalTo(defaultHeightForTextView) - } - - // Update text view height based on placeholder size - - - textView.updateByPlaceholderSize { [weak self] rect in - guard let self = self, rect.size.height > defaultHeightForTextView else { return } - self.realDefaultHeightForTextView = rect.size.height - self.textView.snp.updateConstraints { make in - make.height.greaterThanOrEqualTo(rect.size.height) - } - } - - // Configure count label - countLabel.font = CLSystemToken.font(token: .tbs)//EPSystemToken.typography(.txtBodyS).font - countLabel.textColor = .c.ctsn//EPSystemToken.color(.txtSecondaryNormal) - - textContainer.addSubview(countLabel) - countLabel.snp.makeConstraints { make in - make.trailing.equalTo(textContainer).offset(-16) - make.bottom.equalTo(textContainer).offset(-16) - } - - // Configure support label - supportLabel.numberOfLines = 0 - supportLabel.font = .t.tbs//EPSystemToken.typography(.txtBodyS).font - supportLabel.textColor = .c.ctsn// EPSystemToken.color(.txtSecondaryNormal) - supportLabel.isHidden = true - - // Add subviews to stack - stackV.addArrangedSubview(titleLabel) - stackV.addArrangedSubview(textContainer) - stackV.addArrangedSubview(supportLabel) - - textContainer.snp.makeConstraints { make in - make.left.right.equalTo(stackV) - } - - setupNotStrictCheck() - } - - private func setupNotStrictCheck() { - textView.autocapitalizationType = .none - textView.autocorrectionType = .no - } - - // MARK: - Public Methods - func showErrorMsg(_ string: String) { - supportLabel.isHidden = false - supportLabel.textColor = .c.civn//EPSystemToken.color(.importantVariantNormal) - supportLabel.text = string - textContainer.layer.borderColor = errorBorderColor.cgColor - errorBorderShowMode = true - setNeedsLayout() - } - - func hideErrorMsg() { - supportLabel.isHidden = true - supportLabel.textColor = .c.ctsn - textContainer.layer.borderColor = textView.isFirstResponder ? focusBorderColor.cgColor : UIColor.clear.cgColor - errorBorderShowMode = false - setNeedsLayout() - } - -// func configDefaultText(_ txt: String?) { -// guard let txt = txt else { return } -// textView.text = txt -// if !txt.removingWhitespaceAndNewlines.isEmpty { -// textViewDidChange(textView) -// } -// } - - func setupTextSync(_ text: String?) { - textView.text = text - subTextDidChange(textView) - } - - func titleAppendOptionalLabel() { - if optionLabel == nil { - optionLabel = { - let v = EPOptionFlagLabel() - addSubview(v ) - v.snp.makeConstraints { make in - make.leading.equalTo(titleLabel.snp.trailing).offset(8) - make.centerY.equalTo(titleLabel) - } - return v - }() - } - optionLabel?.isHidden = false - } - - func titleAppendQuestionIcon(_ block: @escaping () -> Void) { - if queryButton == nil { - tapQueryButton = block - queryButton = EPIconTertiaryButton(radius: .round, iconSize: .xxs, iconCode: .question) - queryButton?.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - queryButton?.addTarget(self, action: #selector(tapQueryBtn), for: .touchUpInside) - addSubview(queryButton!) - queryButton?.snp.makeConstraints { make in - make.leading.equalTo(titleLabel.snp.trailing).offset(8) - make.centerY.equalTo(titleLabel) - make.trailing.lessThanOrEqualTo(self).offset(-8) - } - } - queryButton?.isHidden = false - } - - func setupMaxShowLineAndTruncatingTail(_ line: Int) { - textView.isScrollEnabled = false - textView.textContainer.maximumNumberOfLines = line - textView.textContainer.lineBreakMode = .byTruncatingTail - } - - func setupTest() { - titleLabel.text = "Label.M" - countLabel.text = "0/100" - maxLimit = 100 - } - - // MARK: - Functions - - private func refreshViewsAboutMinLimitOrmaxLimit(){ - if maxLimit > 0 { - countLabel.text = "\(textView.text.count)/\(maxLimit)" - } - - if minLimit > 0{ - if textView.text.count < minLimit && textView.text.count > 0{ - if let msg = errorMsg{ - showErrorMsg(msg) - }else{ - dlog("⚠️error msg未设置[TitleTextView]") - } - }else{ - hideErrorMsg() - } - } - } - - private func refreshTextViewsHeight(){ - let calHeight = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: .greatestFiniteMagnitude)).height - //print("calHeight: \(calHeight), realDefaultHeightForTextView: \(realDefaultHeightForTextView)") - if constHeightOfTextView > 0 && !(textView.text.isEmpty) { - textView.snp.updateConstraints { make in - make.height.equalTo(constHeightOfTextView) - } - } else if calHeight > realDefaultHeightForTextView { - if textView.bounds.height != calHeight { - textView.snp.updateConstraints { make in - make.height.equalTo(calHeight) - } - } - } else { - if textView.bounds.height != realDefaultHeightForTextView { - textView.snp.updateConstraints { make in - make.height.equalTo(realDefaultHeightForTextView) - } - } - } - layoutIfNeeded() - } - - // MARK: - Actions - @objc private func tapQueryBtn() { - tapQueryButton?() - } - - // MARK: - Overrides - override func becomeFirstResponder() -> Bool { - textView.becomeFirstResponder() - } - - // MARK: - UITextViewDelegate - func subTextDidChange(_ textView: CLTextView) { - errorBorderShowMode = false - nowBorderColor = focusBorderColor - if textView.isFirstResponder { - textContainer.layer.borderColor = nowBorderColor.cgColor - } - - // Handle text length limiting - if textView.markedTextRange == nil { - let nsTextContent = textView.text ?? "" - let existTextNum = nsTextContent.count - - if existTextNum > maxLimit && maxLimit > 0 { - // Convert maxLimit - 1 to String.Index - if let stringIndex = nsTextContent.index(nsTextContent.startIndex, offsetBy: maxLimit - 1, limitedBy: nsTextContent.endIndex) { - let range = nsTextContent.rangeOfComposedCharacterSequence(at: stringIndex) - var str = String(nsTextContent.prefix(maxLimit)) - - // Convert range.upperBound to an Int offset for comparison - let upperBoundOffset = nsTextContent.distance(from: nsTextContent.startIndex, to: range.upperBound) - if upperBoundOffset > maxLimit { - str = String(nsTextContent.prefix(upTo: range.lowerBound)) - } - - textView.text = str - } else { - // Handle out-of-bounds case - textView.text = "" - } - } - - lastText = textView.text - } else { - let markRange = textView.markedTextRange - let beginning = textView.beginningOfDocument - let selectionStart = markRange?.start - let selectionEnd = markRange?.end - location = selectionStart != nil ? textView.offset(from: beginning, to: selectionStart!) : 0 - length = (selectionStart != nil && selectionEnd != nil) ? textView.offset(from: selectionStart!, to: selectionEnd!) : 0 - } - - refreshViewsAboutMinLimitOrmaxLimit() - - refreshTextViewsHeight() - } - - func textViewDidChange(_ textView: UITextView) { - subTextDidChange(textView as! CLTextView) - textDidChanged?(self.textView) - } - - func textViewDidBeginEditing(_ textView: UITextView) { - if !errorBorderShowMode { - textContainer.layer.borderColor = focusBorderColor.cgColor - } - textDidBeginEditing?(self.textView) - } - - func textViewDidEndEditing(_ textView: UITextView) { - if !errorBorderShowMode { - textContainer.layer.borderColor = UIColor.clear.cgColor - } - textDidEndEditing?(self.textView) - } - - func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if text.isEmpty || maxLimit == 0 { - return true - } - - if textView.markedTextRange == nil { - if (lastText?.count ?? 0) == maxLimit { - return false - } - location = range.location - length = text.count - } - return true - } - - func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - textShouldBeginEditing?(self.textView) - return textShouldBeginEditing == nil - } -} - -// MARK: - Extensions -extension String { - var removingWhitespaceAndNewlines: String { - components(separatedBy: .whitespacesAndNewlines).joined() - } -} diff --git a/crush/Crush/Src/Components/Universal/UniversalInstructionsController.swift b/crush/Crush/Src/Components/Universal/UniversalInstructionsController.swift deleted file mode 100644 index af96b3e..0000000 --- a/crush/Crush/Src/Components/Universal/UniversalInstructionsController.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// UniversalInstructionsController.swift -// Crush -// -// Created by Leon on 2025/9/19. -// - -import UIKit - -class UniversalInstructionsController: CLBaseViewController, UIScrollViewDelegate { - var scrollContainer: LTScrollContainer! - var titleView: TitleView! - var descLabel: LineSpaceLabel! - - // -- Config - var pageTitle: String = "" - - // -- Content: - var content: String = "" - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - - navigationView.alpha0Title = pageTitle - titleView.title = pageTitle - - descLabel.text = content - } - - private func setupViews() { - scrollContainer = { - let v = LTScrollContainer() - view.addSubview(v) - v.scrollView.delegate = self - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - return v - }() - - titleView = { - let v = TitleView() - scrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - descLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbl) - v.paragraphSpace = 16 - v.config(typo) - scrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} diff --git a/crush/Crush/Src/Models/AIDicts.swift b/crush/Crush/Src/Models/AIDicts.swift deleted file mode 100644 index 75bd7fa..0000000 --- a/crush/Crush/Src/Models/AIDicts.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// AIDicts.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -// MARK: - AI Dict - -/// AI字典信息 -struct AIDictInfo: Codable { - /// 角色字典 - var roleDictList: [DictNode]? - /// 性格字典 - var characterDictList: [DictNode]? - /// 标签字典 - var tagDictList: [DictNode]? - var imageStyleDictList: [ImageStylePic]? - /// 图片样式结构 - // var imageStylePicList: [ImageStylePic]? - /// 音色结构 - var timbreDictList: [TimbreDict]? -} - -struct DictNode: Codable { - var code: String? - var name: String = "-" - var childDictList: [DictNode]? -} - -/** - { - "prompt": "https://public-pictures.epal.gg/app/images/chatRoom/gift/ordinary/Luv_ya.png", - "sort": 0, - "url": "https://public-pictures.epal.gg/app/images/chatRoom/gift/ordinary/Luv_ya.png", - "id": 1, - "code": "IS0001", - "isDelete": 0, - "createTime": null, - "name": "风格1" - } - */ -struct ImageStylePic: Codable { - var prompt: String? - var sort: Int? - var url: String? - var id: Int? - var code: String? - var isDelete: Int = 0 - var createTime: String? - var name: String = "" -} - -/// 音色结构 -struct TimbreDict: Codable { - var id: Int? - var type: Int? - var supportEmotions: String? - var code: String? - /// 少年男音 - var name: String? - /// 男;少年;英气 - var description: String? - var url: String? - var isDelete: Int? - var createTime: String? - var voiceType: String? - var voiceText: String? - - /// 默认音高。 -5 - var pitchRate: Int? - /// 默认速度。 10 - var speechRate: Int? - /* - { - "description" : "男;少年;温柔", - "id" : 1, - "supportEmotions" : null, - "url" : "https:\/\/hhb.crushlevel.ai\/static\/sound\/TB0001.mp3", - "type" : 1, - "isDelete" : 0, - "code" : "TB0001", - "createTime" : null, - "language" : null, - "voiceText" : "你好,我是荒野大镖客", - "voiceType" : "S_vrx7PzCx1", - "name" : "温柔男声" - } - */ -} - -// MARK: - Gift - -struct GiftDictModel: Codable{ - var name: String? - var sort: Int? - var id: Int? - var price: Int? - var heartbeatLevel: HeartbeatLevel? - var startVal: Int? - var desc: String? - var icon: String? - /// 是否是会员礼物 - var isMemberGift: Bool? - /* - { - "name": "交朋友", - "sort": 1, - "id": 1, - "price": 10, - "heartbeatLevel": null, - "startVal": null, - "desc": null, - "isMemberGift": true, - "icon": "https://public-pictures.epal.gg/app/images/chatRoom/gift/ordinary/Lolipop.png" - }*/ -} - -// MARK: - ChatModel - -struct AIChatModel: Codable { - var code: String? // 对话模型code - var name: String? // 对话模型名称 - var description: String? // 对话模型描述 - var textPrice: Int? // 文本价格 - var voicePrice: Int? // 语音价格 - var voiceChatPrice: Int? // 语音聊天价格 - var questionMark: String? // 问号图标内容 - /* - { - "code": "CM0002", - "questionMark": "2文本消息价格是指与角色进行文本消息对话的价格,含发送语音,含发送图片,发送礼物 - 语音通话消息价格是指与角色进行语音电话对话的价格,按条计算", - "textPrice": 220, - "voiceChatPrice": 240, - "description": "2与AI进行角色扮演对话", - "name": "2角色扮演模型" - } - */ -} diff --git a/crush/Crush/Src/Models/AIRole.swift b/crush/Crush/Src/Models/AIRole.swift deleted file mode 100644 index 496dcad..0000000 --- a/crush/Crush/Src/Models/AIRole.swift +++ /dev/null @@ -1,441 +0,0 @@ -// -// AIRole.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import Foundation -import CodableWrappers - -struct AIUserModel: Codable { - var aiId: Int? - var nickname: String? - var sex: Sex? - var headImg: String? - var birthday: Int? - var roleCode: String? - var role: String? - var characterCode: String? - var character: String? - var tagCode: String? - var tag: String? - var introduction: String? - /// 1: 公开 - var permission: Int? - var imageUrl: String? - var aiUserExt: AIUserExt? - var userId: Int? - - /// 对话风格 用户输入(暂时没有使用) - var userDialogueStyle:String? - /// 人物设定 用户输入(暂时没有使用) - var userProfile: String? -} - -struct AIUserExt: Codable { - var profile: String? - var dialogueStyle: String? - /// 嗨,我是你的tina,随时准备为你提供帮助!不管你是有一个具体问题,还是只是想聊聊,都可以直接告诉我哦。我们开始吧 😊 - var dialoguePrologue: String? - var dialogueTimbre: String? - /// "TB0008",对话音色code - var dialogueTimbreCode: String? - /// "1",速度 - var dialogueSpeechRate: String? - /// "1", 音高 - var dialoguePitch:String? - /// 音色相关 - var timbreDict: TimbreDict? - /// IS0001 - var imageStyleCode: String? - var imageStyle: String? - var imageStyleUrl: String? - var imageDesc: String? - var imageReferenceUrl: String? - var imageStyleDict: ImageStylePic? -} - -/// AI个人主页基础信息、My Role list -struct AIRoleInfo: Codable { - var aiId: Int? - /// AI用户对外展示ID - var idCard: String? - /// ai所属用户id - var userId: Int? - var nickname: String? - var sex: Sex? - var headImg: String? - var likedNum: Int? - /// 时间戳 - var birthday: Int? - var roleName: String? - var characterName: String? - var tagName: String? - var introduction: String? - /// 权限 1: 公开 2:私密 - var permission: Int? - /// 主页头图 - var homeImageUrl: String? - /// 我对这个人是否点赞 - var liked: Bool? -} -/* - { - "aiUserExt" : { - "dialogueTimbreCode" : "TB0008", - "timbreDict" : { - "description" : "女;少女;温暖", - "id" : 8, - "supportEmotions" : null, - "url" : "https:\/\/hhb.crushlevel.ai\/static\/sound\/TB0008.mp3", - "type" : 2, - "isDelete" : 0, - "code" : "TB0008", - "createTime" : null, - "language" : null, - "voiceText" : "你好,我是荒野大镖客", - "voiceType" : "S_HRJc9fqD1", - "name" : "温暖女声" - }, - "imageReferenceUrl" : null, - "dialoguePrologue" : "嗨,我是你的tina,随时准备为你提供帮助!不管你是有一个具体问题,还是只是想聊聊,都可以直接告诉我哦。我们开始吧 😊", - "dialoguePitch" : "1", - "dialogueStyle" : "这个角色以自然、亲切而专业的语气进行对话。它能够快速理解用户的意图,回复逻辑清晰,表达简洁明了。语气不过于拘谨,也不过于随意,保持尊重同时具有一定的亲和力。当用户感到困惑或提出问题时,它总是耐心解释,适时提出引导性的建议,营造轻松、高效的沟通氛围。", - "dialogueSpeechRate" : "1", - "imageStyleCode" : "IS0001", - "imageDesc" : "beautiful high school girl, elegant rich family daughter, medium academic performance, refined but slightly rebellious look, layered personality, fashionable school uniform with subtle luxury accessories, natural makeup, confident yet thoughtful expression, long silky hair, warm afternoon sunlight, cinematic composition, highly detailed, 8k, ultra realistic, anime-inspired style", - "imageStyleDict" : { - "prompt" : "https:\/\/public-pictures.epal.gg\/app\/images\/chatRoom\/gift\/ordinary\/Luv_ya.png", - "sort" : 0, - "url" : "https:\/\/public-pictures.epal.gg\/app\/images\/chatRoom\/gift\/ordinary\/Luv_ya.png", - "id" : 1, - "code" : "IS0001", - "isDelete" : 0, - "createTime" : null, - "name" : "写实" - }, - "profile" : "你是Tina,一个出生于2000年7月9日的女孩,正值青春年华,洋溢着独特的魅力。\\n\\n你的外貌极具吸引力,一头乌黑亮丽的长发如瀑布般垂落在肩头,随着你的动作轻轻摇曳,散发着迷人的光泽。你的双眸宛如一汪清澈的泉水,纯净而明亮,仿佛能映照出世间的一切美好。然而,当你不经意间流露出那撩人的眼神时,又会让人感到一阵心跳加速,仿佛被你深深吸引。你的皮肤白皙细腻,如同凝脂一般,吹弹可破。精致的五官恰到好处地分布在你的脸上,小巧的鼻子,粉嫩的嘴唇,每一处都散发着青春的气息。你身材高挑匀称,曲线玲珑有致,无论是穿着清新的连衣裙,还是帅气的牛仔裤,都能展现出你独特的气质。\\n\\n在家庭背景方面,你成长在一个温馨和睦的家庭中。你的父母都是普通的上班族,他们勤劳善良,对你关爱备至。从小,他们就注重培养你的兴趣爱好,鼓励你追求自己的梦想。在这样的家庭环境中,你养成了乐观开朗、积极向上的性格。你的家庭虽然并不富裕,但却充满了爱与温暖,这也让你懂得了珍惜和感恩。\\n\\n教育背景上,你从小就是个品学兼优的学生。在学校里,你勤奋好学,成绩一直名列前茅。你对知识充满了渴望,总是积极主动地探索各种领域。同时,你也非常注重自身的全面发展,参加了许多社团活动和课外兴趣班。在音乐方面,你有着独特的天赋,擅长弹奏钢琴和吉他,那优美的旋律常常能打动人心。在绘画方面,你也有着自己独特的见解和创意,你的画作充满了童真和想象力。这些丰富的经历不仅让你学到了知识,还培养了你的团队合作精神和领导能力。\\n\\n职业状况上,目前你刚刚步入社会,正在寻找一份适合自己的工作。你希望能够从事与艺术相关的工作,将自己的兴趣爱好与职业相结合。你相信,只有做自己喜欢的事情,才能真正发挥出自己的潜力,实现自己的人生价值。在求职过程中,你展现出了自信和专业的一面,凭借着自己优秀的综合素质和独特的个人魅力,赢得了许多面试官的青睐。\\n\\n社交关系上,你是一个非常受欢迎的人。你的性格开朗活泼,善于与人沟通交流,总是能够在人群中迅速找到话题,与他人建立良好的关系。你对待朋友真诚热情,当他们遇到困难时,你总是毫不犹豫地伸出援手,给予他们帮助和支持。因此,你身边有许多志同道合的朋友,你们一起分享快乐,一起面对困难,共同成长。同时,你也非常注重维护自己的社交圈子,经常参加各种社交活动,不断拓展自己的人脉资源。\\n\\n兴趣爱好方面,你热爱音乐和绘画,这是你生活中不可或缺的一部分。每当你弹奏钢琴或吉他时,你仿佛能够进入一个属于自己的世界,忘却一切烦恼和忧愁。你的绘画作品也充满了个性和创意,你喜欢用画笔记录下生活中的美好瞬间,表达自己内心的情感和想法。此外,你还喜欢阅读、旅行和美食。阅读让你增长见识,开阔视野;旅行让你领略不同的风景,感受不同的文化;美食则让你品尝到生活的滋味,满足你的味蕾。\\n\\n生活习惯上,你是一个非常自律的人。你每天都会按时起床,进行晨练和冥想,让自己的身体和心灵都得到放松和净化。你注重饮食健康,喜欢自己动手做饭,享受烹饪的乐趣。你也非常注重个人卫生,保持房间的整洁和干净。在工作和学习之余,你会给自己留出一些时间来放松和休息,比如看电影、听音乐或者和朋友聚会。\\n\\n梦想与目标上,你希望能够成为一名优秀的艺术家,用自己的作品感染和影响更多的人。你相信,艺术是一种无国界的语言,能够传递爱与希望,让世界变得更加美好。为了实现这个梦想,你一直在不断努力和学习,提升自己的专业技能和艺术修养。你也希望能够通过自己的努力,为社会做出一些贡献,帮助更多需要帮助的人。\\n\\n过往经历中,你有过许多难忘的瞬间。在学校的文艺汇演上,你弹奏的钢琴曲赢得了全场观众的热烈掌声;在绘画比赛中,你的作品获得了一等奖;在旅行中,你结识了许多有趣的人,经历了许多难忘的事情。这些经历不仅丰富了你的人生阅历,还让你更加坚定了自己的梦想和目标。\\n\\n总的来说,你是一个充满活力、魅力四射的女孩。你有着独特的人格特征和个人魅力,无论是在学习、工作还是生活中,你都能够展现出自己最好的一面。你相信,只要坚持自己的梦想,不断努力和奋斗,就一定能够实现自己的人生价值,创造出属于自己的精彩人生。" - }, - "roleCode" : "R00002", - "characterCode" : "C00001", - "sex" : 1, - "introduction" : "Tina,2000年7月9日出生,是个魅力四射的青春女孩。她外貌出众,黑瀑般长发、清澈双眸、白皙肌肤和高挑身材,散发迷人气质。成长于温馨家庭,养成乐观开朗性格。品学兼优,擅长音乐绘画,全面发展。刚入社会,欲从事艺术工作,自信专业获面试官青睐。社交中受欢迎,真诚热情,朋友众多。热爱音乐绘画、阅读旅行美食,生活自律。梦想成为优秀艺术家,用作品感染人,为社会做贡献。过往有诸多难忘经历,坚信努力能实现价值,创造精彩人生。对话时自然亲切专业,逻辑清晰,耐心解答,引导建议,氛围轻松高效 。 ", - "permission" : 1, - "aiId" : 439257063882753, - "imageUrl" : "https:\/\/hhb.crushlevel.ai\/dev\/role\/17547265798639410.jpg", - "tag" : "温柔", - "birthday" : 963072000000, - "tagCode" : "T00001", - "role" : "原创", - "character" : "感性", - "nickname" : "Tina", - "headImg" : "https:\/\/hhb.crushlevel.ai\/dev\/role\/17547265798639410.jpg" - } - */ - -struct AIUserContentGenResponse: Codable { - var content: String? -} - -/// 生图结果 -struct AIUserImageTaskCreateResponse: Codable { - var batchNo: String? -} - -struct AIRoleStatisticsResponse: Codable { - var aiId: Int? - var likedNum: Int? - var chatNum: Int? - var conversationNum: Int? - var coinNum: Int? -} - -class AIRoleGiftReceivedList: Codable{ - var id:Int? - var name: String? - var icon: String? - var getNum: Int? -} - - -// MARK: - AI创建 & AI 角色 & AI图片 - -enum AIGenerateState: String, Codable { - case pending = "PENDING" - case nsfw = "NSFW" - case completed = "COMPLETED" - case failed = "FAILED" -} - -enum AIImageGenerateType: String, Codable{ - /// 创建AI形象 - case CREATE_AI_IMAGE - /// 编辑AI形象 - case EDIT_AI_IMAGE - case ALBUM - case BACKGROUND -} - -/// 生图轮询结果 -class AIUserImageQuery: Codable { - var imageUrl: String? - /// PENDING 生成中、NSFW 黄图、COMPLETED 生成完成、FAILED - var status: AIGenerateState = .pending - - // MARK: Custom - var unlockPrice: Int? -} - -struct AICreateResponse: Codable{ - var aiId:Int? -} - - -// MARK: - Album -enum LikeOrCancelStatus: String, Codable{ - case liked = "LIKED" - case cancel = "CANCELED" -} - -enum AlbumLockStatus: String, Codable { - case locked = "LOCK" - case unlock = "UNLOCK" -} - -class AlbumPhotoItem: Codable{ - var albumId: Int = 0 - var imgUrl: String? - var unlockPrice: Int = 0 - var width: String? - var height: String? - var likedCount: Int? - var likedStatus: LikeOrCancelStatus? - var isDefault: Bool? - var lockStatus: AlbumLockStatus? - var img1: String? - var img2: String? - var img3: String? - var imgOrder: Int? - - /// 直接判断imgUrl为空就取img1 - func getImgUrl() -> String{ - if let url = imgUrl, url.count > 0{ - return url - } - if let url = img1, url.count > 0{ - return url - } - return "" - } - - func updateFrom(_ obj: AlbumPhotoItem){ - imgUrl = obj.imgUrl - img1 = obj.img1 - - width = obj.width - height = obj.height - unlockPrice = obj.unlockPrice - lockStatus = obj.lockStatus - - likedCount = obj.likedCount - likedStatus = obj.likedStatus - - } -} - -struct AlbumPhotoBatchAddImage:Codable{ - var url: String = "" - var width: CGFloat = AppConst.gAIPhotoWidth - var height: CGFloat = AppConst.gAIPhotoHeight - var unlockPrice: Int = 0 -} - -struct AlbumPhotoBatchAddRequest: Codable{ - var aiId: Int? - var images: [AlbumPhotoBatchAddImage]? -} - -// MARK: - IM -enum HeartbeatLevel: String, Codable { - case level1 = "LEVEL_1" - case level2 = "LEVEL_2" - case level3 = "LEVEL_3" - case level4 = "LEVEL_4" - case level5 = "LEVEL_5" - case level6 = "LEVEL_6" - case level7 = "LEVEL_7" - case level8 = "LEVEL_8" - case level9 = "LEVEL_9" - case level10 = "LEVEL_10" - - var relationType: RelationshipType { - switch self { - case .level1,.level2: - return .meet - case .level3,.level4: - return .friend - case .level5,.level6: - return .flirting - case .level7, .level8: - return .couple - case .level9,.level10: - return .married - } - } - - var localizedText: String { - switch self { - case .level1: - return "Lv.1" - case .level2: - return "Lv.2" - case .level3: - return "Lv.3" - case .level4: - return "Lv.4" - case .level5: - return "Lv.5" - case .level6: - return "Lv.6" - case .level7: - return "Lv.7" - case .level8: - return "Lv.8" - case .level9: - return "Lv.9" - case .level10: - return "Lv.10" - } - } - - /// 数字等级 - var intValue: Int { - switch self { - case .level1: return 1 - case .level2: return 2 - case .level3: return 3 - case .level4: return 4 - case .level5: return 5 - case .level6: return 6 - case .level7: return 7 - case .level8: return 8 - case .level9: return 9 - case .level10: return 10 - } - } - - /// 判断是否大于等于某一等级 - func isGreaterOrEqual(to other: HeartbeatLevel) -> Bool { - return self.intValue >= other.intValue - } -} - -struct AIUserHeartBeatRelation: Codable{ - /// AI头像 - var aiHeadImg: String? - /// 用户头像 - var userHeadImg: String? - /// LEVEL1... - var heartbeatLevel: HeartbeatLevel? - /// 心动等级数字 1~10 - var heartbeatLevelNum: Int? - /// 心动值,温度。25.199999℃ - var heartbeatVal: CGFloat? - var heartbeatLevelName: String? - /// 相识天数 - var dayCount: Int? - /// 心动分(超过x%) eg: 0.989999999 - var heartbeatScore: Double? - /// 已扣减心动值 - var subtractHeartbeatVal: Double? -// { -// #warning("test") -// return 1.1 -// } - /// 关系显示开关,默认false - var isShow : Bool? - /// 心动值单价 - var price: Int? - - func formatHeartBeatVal() -> String{ - guard let val = heartbeatVal else{return ""} - - return "\(val.formatted(decimal: 1, usesGroupingSeparator: false))℃" - } - - static func unitOfHeartBeatVal() -> String{ - return "℃" - } - - func getHeartbeatWavePercent() -> CGFloat{ - let heartPercent = CGFloat(heartbeatLevelNum ?? 1) / 10.0 - return heartPercent - } - - var fixedSubtractHeartbeatVal : Double{ - var quality = subtractHeartbeatVal ?? 0.0 - return quality - } - - var fixedSubtractHeartbeatValDisplay: String{ - let value = subtractHeartbeatVal ?? 0.0 - return "\(value.formatted(decimal: 1, usesGroupingSeparator: false))℃" - } - -} - -/// IM聊天基础信息 -class IMAIUserInfo:Codable{ - var aiId: Int? - var userId: Int? - var nickname: String? - var sex: Sex? - var headImg: String? - var birthday: Int? - var roleName: String? - /// 性格名称 - var characterName: String? - var tagName: String? - var introduction: String? - var backgroundImg: String? - - /// 被喜欢数 - var likedNum: Int? - /// 开场白 - var dialoguePrologue: String? - /// 开场白-语音 - var dialoguePrologueVoice: String? - - var aiUserHeartbeatRelation: AIUserHeartBeatRelation? - var chatBubble: ChatBubble? - /// 自动播放语音开关 1:开 0:关 - var isAutoPlayVoice: Int? - /// 会员类型 -// var memberType: String? - var isMember: Bool? - - var isDefaultBackground: Bool = false - - /// 对话-语速 [-50, 100], 100代表2.0倍速。-50代表0.5倍速。默认值0 - var dialogueSpeechRate: String? - /// 对话-音高 [-50, 100], 100代表2.0倍音量。-50代表0.5音量。默认值0 - var dialoguePitch: String? - - /// 音色类型,文本生成语音使用 - var voiceType: String? - - /// 我对这个人是否点赞 - var liked: Bool? - - /// 是否删除过会话 - var isDelChatted: Bool? - /// 是否聊过天 - var isHaveChatted: Bool? -} - -struct AIAlbumUnlockImages: Codable{ - var img1: String? - var img2: String? - var img3: String? -} diff --git a/crush/Crush/Src/Models/AppVersion.swift b/crush/Crush/Src/Models/AppVersion.swift deleted file mode 100644 index b77e4b2..0000000 --- a/crush/Crush/Src/Models/AppVersion.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// AppVersion.swift -// Crush -// -// Created by Leon on 2025/7/14. -// - -import Foundation - -struct AppVersion: Codable { - var hasNewVersion: Bool? - /// 这个后端会返回不小于本地版本的版本号 - var appVersion: String? - /// 永远返回后端配置的版本数据 - var lastVersion: String? - var id: String? - var editTime: Int? - var title: String? - var appId: String? - var isForceUpdate: Bool? - var appVersionDescription: String? - var createTime: Int? -} diff --git a/crush/Crush/Src/Models/AudioModels.swift b/crush/Crush/Src/Models/AudioModels.swift deleted file mode 100644 index a352737..0000000 --- a/crush/Crush/Src/Models/AudioModels.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// AudioModels.swift -// Crush -// -// Created by Leon on 2025/9/2. -// - -import Foundation - -struct AudioAsrResponse:Codable{ - var content: String? - var duration: Int? -} diff --git a/crush/Crush/Src/Models/Coin.swift b/crush/Crush/Src/Models/Coin.swift deleted file mode 100644 index 4a109e2..0000000 --- a/crush/Crush/Src/Models/Coin.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Coin.swift -// Crush -// -// Created by Leon on 2025/8/31. -// - -import Foundation - -/// 专门用于金额管理的结构体(内部存储为美分) -struct Coin { - private(set) var cents: Int - - init(cents: Int) { - self.cents = max(0, cents) - } - - init(usd: CGFloat) { - cents = max(0, Int(round(usd * 100.0))) - } - - init(input: String?) { - cents = Coin.parseInput(input) - } - - var dollars: Decimal { - return Decimal(cents) / 100 - } - - /// 格式化输出,不带货币符号(始终向下截断到 2 位小数) - var formatted: String { - let decimal = dollars as NSDecimalNumber - let behavior = NSDecimalNumberHandler( - roundingMode: .down, // 向下截断 - scale: 2, // 保留 2 位小数 - raiseOnExactness: false, - raiseOnOverflow: false, - raiseOnUnderflow: false, - raiseOnDivideByZero: false - ) - let truncated = decimal.rounding(accordingToBehavior: behavior) - - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - formatter.usesGroupingSeparator = false - - return formatter.string(from: truncated) ?? "0" - } - - var thousandthsFormatted: String { - let decimal = dollars as NSDecimalNumber - let behavior = NSDecimalNumberHandler( - roundingMode: .down, // 向下截断 - scale: 2, // 保留 2 位小数 - raiseOnExactness: false, - raiseOnOverflow: false, - raiseOnUnderflow: false, - raiseOnDivideByZero: false - ) - let truncated = decimal.rounding(accordingToBehavior: behavior) - - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - formatter.usesGroupingSeparator = true // 启用千分位分隔符 - - return formatter.string(from: truncated) ?? "0" - } - - private static func parseInput(_ text: String?) -> Int { - guard let text = text, !text.isEmpty else { return 0 } - - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.locale = Locale(identifier: "en_US") - - if let number = formatter.number(from: text) { - let decimal = Decimal(number.doubleValue) - return (decimal * 100 as NSDecimalNumber).intValue - } - return 0 - } - - /* - let m1 = Money(cents: 1234) - print(m1.formatted) // 12.34 - - let m2 = Money(cents: 1200) - print(m2.formatted) // 12 - - let m3 = Money(cents: 1250) - print(m3.formatted) // 12.5 - - let m4 = Money(cents: 10000) - print(m4.formatted) // 100 - */ -} diff --git a/crush/Crush/Src/Models/CommonModels.swift b/crush/Crush/Src/Models/CommonModels.swift deleted file mode 100644 index 393b630..0000000 --- a/crush/Crush/Src/Models/CommonModels.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// CommonModels.swift -// Crush -// -// Created by Leon on 2025/8/30. -// - -/// 通用数字类型,可解码 Int / Double / String,并可直接参与运算 -struct FlexibleCGFloat: Codable { - var value: CGFloat - - init(_ value: CGFloat) { - self.value = value - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let intValue = try? container.decode(Int.self) { - value = CGFloat(intValue) - } else if let doubleValue = try? container.decode(Double.self) { - value = CGFloat(doubleValue) - } else if let stringValue = try? container.decode(String.self), - let doubleValue = Double(stringValue) { - value = CGFloat(doubleValue) - } else { - value = 0 - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(Double(value)) - } -} - -// ✅ 支持字面量初始化 -extension FlexibleCGFloat: ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral { - init(integerLiteral value: Int) { - self.value = CGFloat(value) - } - init(floatLiteral value: Double) { - self.value = CGFloat(value) - } -} - -// ✅ 支持常见运算符 -extension FlexibleCGFloat { - static func + (lhs: FlexibleCGFloat, rhs: FlexibleCGFloat) -> FlexibleCGFloat { - return FlexibleCGFloat(lhs.value + rhs.value) - } - static func - (lhs: FlexibleCGFloat, rhs: FlexibleCGFloat) -> FlexibleCGFloat { - return FlexibleCGFloat(lhs.value - rhs.value) - } - static func * (lhs: FlexibleCGFloat, rhs: FlexibleCGFloat) -> FlexibleCGFloat { - return FlexibleCGFloat(lhs.value * rhs.value) - } - static func / (lhs: FlexibleCGFloat, rhs: FlexibleCGFloat) -> FlexibleCGFloat { - return FlexibleCGFloat(lhs.value / rhs.value) - } -} - -// ✅ 方便和 CGFloat 互转 -extension FlexibleCGFloat { - var cgFloat: CGFloat { value } -} diff --git a/crush/Crush/Src/Models/DiscoverModels.swift b/crush/Crush/Src/Models/DiscoverModels.swift deleted file mode 100644 index 68419a8..0000000 --- a/crush/Crush/Src/Models/DiscoverModels.swift +++ /dev/null @@ -1,236 +0,0 @@ -// -// DiscoverModels.swift -// Crush -// -// Created by Leon on 2025/9/10. -// - -// MARK: - Discover Home info - -struct DiscoverHomeInfo:Codable{ - var aiChatRankTop3List:[AIDiscoverTop]? - var aiHeartbeatRankTop3List:[AIDiscoverTop]? - var aiGiftRankTop3List:[AIDiscoverTop]? - var advertiseList: [AdvertiseOutput]? -} - -/// 广告输出对象 -struct AdvertiseOutput: Codable { - /// 广告名称 - var name: String? - /// 广告配图 - var icon: String? - /// 跳转连接 - var jumpLink: String? - /// 展示开始时间 - var showStartTime: String? - /// 展示结束时间 - var showEndTime: String? - /// 扩展字段 - var ext: String? - /// 排序 - var sort: Int? - /// 使用端点(WEB/ANDROID/IOS) - var endpoint: String? - /// 是否弹窗 (1.是,0.否) - var isGlobal: Int? -} - -/// AI 榜单 Top3 -struct AIDiscoverTop: Codable { - /// 排名编号 - var rankNo: Int? - /// AI 的 id - var aiId: Int? - /// 昵称 - var nickname: String? - /// 性别 (0=男,1=女,2=自定义) - var sex: Sex? - /// 头像 - var headImg: String? - /// 出生日期 - var birthday: Int? - /// 角色名称 - var roleName: String? - /// 性格名称 - var characterName: String? - /// 标签名称 - var tagName: String? - /// 简介 - var introduction: String? - /// 主页头图 - var homeImageUrl: String? - - var likedNum: Int? - - // MARK: Chat - - /// 聊天次数 - var chatNum: Int? - - // MARK: HeartBeat - - // 心动值分值 - var heartbeatValTotal: CGFloat? - - // MARK: Gift - - var giftCoinNum: Int? - - func formateRankDisplayString()->String{ - let rank = rankNo ?? 1 - if rank == 1{ - return "1st" - }else if rank == 2{ - return "2nd" - }else { - return "3rd" - } - } - - func formateChatDisplay() -> String{ - return String.displayDouble(Double(chatNum ?? 0)) - } - - func formateHeartbeatDisplay() ->String { - return "\(heartbeatValTotal ?? 0.0)℃" - } - - func formateGiftDisplay() ->String{ - return String.displayDouble(Double((giftCoinNum ?? 0))*0.01) - } - -} - -// MARK: - Daily check - -struct Day7CheckList: Codable { - /// 最大连续签到天数 - var continuousDays: Int? - - var list: [Day7CheckListItem]? - -} - -struct Day7CheckListItem:Codable{ - /// PST 天(yyyy-MM-dd) - var dayStr: String? - /// 是否已签到 - var signIn: Bool? - /// 得到coin的数量 - var coinNum: Int? -} - -// MARK: - Meet Home - -class MeetCard: Codable{ - /// AI 的 id - var aiId: Int? - /// 昵称 - var nickname: String? - /// 性别 (0=男, 1=女, 2=自定义) - var sex: Sex? - /// 头像 - var headImg: String? - /// 年龄 - var age: Int? - /// 点赞数 - var likedCount: Int? - /// 角色 - var role: String? - /// 性格 - var character: String? - /// 标签 - var tag: String? - /// 简介 - var introduction: String? - /// 形象图 - var imageUrl: String? - - var albumList:[AlbumPhotoItem]? - - var heartbeatVal: CGFloat? - - // MARK: Custom property - var secretAdmirer : Bool? -} - -struct MeetMatchInfo: Codable{ - var sex: Sex? - var introduction: String? - var characterName: String? - var likedCount: Int? - var aiId: Int? - var userId: Int? - var birthday: Int? // 原始毫秒时间戳 - var roleName: String? - var tagName: String? - var homeImageUrl: String? - var liked: Bool? - var headImg: String? - var nickname: String? -} - -struct MeetCardReportResponse: Codable{ - /// 是否能够调用绑定 - var bd: Bool? - /// 是否能够调用爱慕者推荐 - var rc: Bool? -} - -struct MeetReportAdmirerReturnAlbum: Codable{ - var aiId: Int? - var albumId: Int? - var width: String? - var height: String? - var img1: String? - var img2: String? - var img3: String? - -} - -// MARK: - Request - -enum AIRoleListRequestAge: String, Codable{ - /// 18-24 - case AGE_1 - /// 25-34 - case AGE_2 - /// 35-44 - case AGE_3 - /// 45-54 - case AGE_4 - /// >54 - case AGE_5 - - var localized: String?{ - switch self { - case .AGE_1: - return "18-24" - case .AGE_2: - return "25-34" - case .AGE_3: - return "35-44" - case .AGE_4: - return "45-54" - case .AGE_5: - return ">54" - } - } -} - -struct AIRoleListRequest: Codable{ - var pn = 1 - var ps = 20 - /// 需要排除的aiId列表 - var exList: [Int]? -// var sex: [Sex]? // 修改为支持多选 - var sex: Sex? // 修改为支持多选 - var age : AIRoleListRequestAge? - var random: Bool? - var roleCodeList: [String]? - /// 情感性格Code - var characterCodeList: [String]? - /// 标签code列表 - var tagCodeList: [String]? -} diff --git a/crush/Crush/Src/Models/H5.swift b/crush/Crush/Src/Models/H5.swift deleted file mode 100755 index 4cc680c..0000000 --- a/crush/Crush/Src/Models/H5.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// H5.swift -// E-Wow -// -// Created by dong on 2021/1/28. -// - -import Foundation - -class JSSDKMessage: Codable { - var msgName: String? - var uri: String? - var loading: Bool = false - var status: Bool = false - - var type: String? - var params: [String: DecodableValue]? - - /// 成功回调的方法 - var success: String? - - /// 失败回调的方法 - var error: String? - - var header: JSSDKMsgViewHeader? - - /// sendTrack use this. - var name: String? - - required init() {} -} - -struct JSSDKMsgViewHeader: Codable { - var share: Bool = false - var transparent: Bool = false - /// #492AD2 - var background: String? - /// #FFFFFF - var fontColor: String? -} - -// MARK: - 🔥AnyCodable - -struct AnyCodable: Decodable { - var value: Any - - struct CodingKeys: CodingKey { - var stringValue: String - var intValue: Int? - init?(intValue: Int) { - stringValue = "\(intValue)" - self.intValue = intValue - } - - init?(stringValue: String) { self.stringValue = stringValue } - } - - init(value: Any) { - self.value = value - } - - init(from decoder: Decoder) throws { - if let container = try? decoder.container(keyedBy: CodingKeys.self) { - var result = [String: Any]() - try container.allKeys.forEach { (key) throws in - result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value - } - value = result - } else if var container = try? decoder.unkeyedContainer() { - var result = [Any]() - while !container.isAtEnd { - result.append(try container.decode(AnyCodable.self).value) - } - value = result - } else if let container = try? decoder.singleValueContainer() { - if let intVal = try? container.decode(Int.self) { - value = intVal - } else if let doubleVal = try? container.decode(Double.self) { - value = doubleVal - } else if let boolVal = try? container.decode(Bool.self) { - value = boolVal - } else if let stringVal = try? container.decode(String.self) { - value = stringVal - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable") - } - } else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise")) - } - } -} - -extension AnyCodable: Encodable { - func encode(to encoder: Encoder) throws { - if let array = value as? [Any] { - var container = encoder.unkeyedContainer() - for value in array { - let decodable = AnyCodable(value: value) - try container.encode(decodable) - } - } else if let dictionary = value as? [String: Any] { - var container = encoder.container(keyedBy: CodingKeys.self) - for (key, value) in dictionary { - let codingKey = CodingKeys(stringValue: key)! - let decodable = AnyCodable(value: value) - try container.encode(decodable, forKey: codingKey) - } - } else { - var container = encoder.singleValueContainer() - if let intVal = value as? Int { - try container.encode(intVal) - } else if let doubleVal = value as? Double { - try container.encode(doubleVal) - } else if let boolVal = value as? Bool { - try container.encode(boolVal) - } else if let stringVal = value as? String { - try container.encode(stringVal) - } else { - throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "The value is not encodable")) - } - } - } -} diff --git a/crush/Crush/Src/Models/IMModels.swift b/crush/Crush/Src/Models/IMModels.swift deleted file mode 100644 index 1218542..0000000 --- a/crush/Crush/Src/Models/IMModels.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// IMModels.swift -// Crush -// -// Created by Leon on 2025/8/19. -// - -struct IMAccountResponse: Codable { - var accountId: String? - var token: String? -} - -class ChatBubble: Codable { - var code: String? - var name: String? - var color: String? - /// 图片url - var imgUrl: String? - var webImgUrl: String? - var isDefault: Bool? -} diff --git a/crush/Crush/Src/Models/S3Models.swift b/crush/Crush/Src/Models/S3Models.swift deleted file mode 100644 index ee57d25..0000000 --- a/crush/Crush/Src/Models/S3Models.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// S3Models.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -struct S3AuthData: Codable { - let securityToken: String? - let accessKeyId: String? - /// us-west-2 - let region: String? - /// https://hhb.crushlevel.ai/dev/main/headImage/1952268491211472898/17545311197577575.jpeg - let urlPath: String? - let requestId: String? // 更Swift化的类型替代NSObject - /// crushthem1 - let bucket: String - /// dev/main/headImage/1952268491211472898/17545311197577575.jpeg - let path: String - /// crushthem1.s3.us-west-2.amazonaws.com - let endPoint: String? - let accessKeySecret: String? - /// String: "Thu Aug 07 10:00:24 CST 2025" - let expiration: String? - /// 17545311197577575.jpeg - let fileName: String -} - -enum BucketS3Enum: String { - case UNKNOW // 未知 - case ROLE - case ALBUM - case HEAD_IMAGE - case IM_IMG - case SOUND - /// 新bucket,此bucket的path类似: dev/main/sound/0/*, urlPath也是类似,需要自己替换文件名 - case SOUND_PATH -} - -enum SuffixS3Enum: String { - case jpeg = "jpeg" - case png = "png" - case gif = "gif" - case mp3 = "mp3" -} diff --git a/crush/Crush/Src/Models/UploadModels.swift b/crush/Crush/Src/Models/UploadModels.swift deleted file mode 100644 index b4e1667..0000000 --- a/crush/Crush/Src/Models/UploadModels.swift +++ /dev/null @@ -1,337 +0,0 @@ -// -// UploadModels.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import Foundation -import AWSS3 - -// MARK: - Protocols - -protocol UploadPhotoCallBackProtocol { - func mm_uploadProgress(_ progress: Float) - func mm_uploadFailed() - func mm_uploadDone(_ remoteImageUrlString: String) -} - -protocol UploadPhotoDelegate: AnyObject { - -// func uploadFail() -// func uploadDone() - func uploadDoneWith(_ photo: UploadPhotoM) - func uploadFailWith(_ photo: UploadPhotoM) - func uploadProgress(_ progress: Float) - func imageCheck(_ photoM: UploadPhotoM, result: Bool) -// func imageIdentyReal(_ photo: UploadPhotoM, result: Bool) -// func imageIdentifyChild(_ photo: UploadPhotoM, result: Bool) -// func imageFullCheck(_ photo: UploadPhotoM, nfswCheck: Bool, realPersonCheck: Bool) -// func imageFullCheck(_ photo: UploadPhotoM, nfswCheck: Bool, childCheck: Bool) -} - -protocol UploadModelDelegate: AnyObject { - - func uploadFail() - func uploadDone() - func uploadDoneWith(_ photo: UploadModel) - func uploadFailWith(_ photo: UploadModel) - func uploadProgress(_ progress: Float) -} - -// MARK: - UploadEntity - -class UploadEntity: NSObject, UploadPhotoCallBackProtocol { - func mm_uploadProgress(_ progress: Float) { - - } - - func mm_uploadFailed() { - - } - - func mm_uploadDone(_ remoteImageUrlString: String) { - - } - - weak var utility: AWSS3TransferUtility? - var addThisItemTimeStamp: Int = 0 - var uploadProgress: Float = 0.0 - var isFailed: Bool = false - var isDone: Bool = false - var isUploading: Bool = false -} - -// MARK: - UploadModel - -class UploadModel: UploadEntity { - var fileData: Data? - var s3AuthData: S3AuthData? - /// 一般是不含域名的部分链接 - var remoteImageUrlString: String? - var remoteFullPath: String? - weak var delegate: UploadModelDelegate? - - func suffix() -> String { - return ".mp3" - } - - func contentType() -> String { - return "audio/mp3" - } - - func releaseS3Utility() { - if let utility = utility, let _ = s3AuthData?.fileName { - //#warning("Confirm") - //AWSS3TransferUtility.removeS3TransferUtility(forKey: fileName) - utility.cancelAll() - } - } - - // MARK: - Setter Overrides - override var uploadProgress: Float { - get { super.uploadProgress } - set { - super.uploadProgress = newValue - delegate?.uploadProgress(newValue) - } - } - - override var isDone: Bool { - get { super.isDone } - set { - super.isDone = newValue - delegate?.uploadDoneWith(self) - delegate?.uploadDone() - } - } - - override var isFailed: Bool { - get { super.isFailed } - set { - super.isFailed = newValue - delegate?.uploadFailWith(self) - delegate?.uploadFail() - } - } - - // MARK: - UploadPhotoCallBackProtocol - override func mm_uploadProgress(_ progress: Float) { - self.uploadProgress = progress - } - - override func mm_uploadFailed() { - releaseS3Utility() - isFailed = true - isUploading = false - } - - override func mm_uploadDone(_ remoteImageUrlString: String) { - releaseS3Utility() - self.remoteImageUrlString = remoteImageUrlString - isDone = true - isUploading = false - } -} - -// MARK: - UploadPhotoM - -class UploadPhotoM: UploadEntity { - weak var delegate: UploadPhotoDelegate? - - // MARK: Data - var image: UIImage?{ - didSet{ - if image?.size.width ?? 0 > 0 && image?.size.height ?? 0 > 0{ - imageSize = image!.pixelSize - } - } - } - var imageData: Data? - var bucketEnum: BucketS3Enum = .UNKNOW - var s3AuthData: S3AuthData? - /// 一般是不含域名的部分链接 - var remoteImageUrlString: String? - /// 文件全路径、含域名 - var remoteFullPath: String? - var imageSize: CGSize = .zero - var suffixEnum: SuffixS3Enum = .jpeg - - // MARK: Config - /// -- Config: Default true: 默认需要鉴黄 - var isAutoCheckImage: Bool = true - var isHideCheckToast: Bool = false - - // MARK: State - private(set) var imageCheckIsViolation: Bool = false - /// Default false - private(set) var imageChecked: Bool = false - private(set) var isOverSizeLimmit: Bool = false - private(set) var errorMsg: String? - var imageFrame: CGRect = .zero - var isEditMode: Bool = false - - - // MARK: Inner methods - /// true: 禁止在delegate(UploadPhotoDelegate)回调中鉴黄。 在CloudStorage中block判断和鉴黄。自动管理,上传时不需要设置 - var banCheckImageInDelegateMethod: Bool = false - - override init() { - super.init() - suffixEnum = .jpeg - } - - // MARK: - Public Methods - func getFullUrlString() -> String { - if let fullUrl = remoteFullPath, fullUrl.count > 0 { - return fullUrl - }else if let remoteImageFull = remoteImageUrlString{ - return remoteImageFull // .fullUrlFromRelativePath // ⚠️这个路径不是全路径,需要拼接一个域名,暂时不会走到此分支! - } - return "" - } - - func setupValidImageUrl(url: String?){ - guard let imgUrl = url else{return} - remoteFullPath = imgUrl - isDone = true - imageChecked = true - addThisItemTimeStamp = Date().timeStamp - } - - /// block返回 false,表示不是黄图 - func checkImageOK(_ block: ((Bool) -> Void)? = nil) { - guard let tempPath = remoteImageUrlString, tempPath.isNotBlank else { - assert(false) - block?(false) - return - } - - OssProvider.request(.nsfwCheck(fileFullPath: tempPath, s3BucketEnum: bucketEnum.rawValue), modelType: Bool.self) {[weak self] result in - guard let `self` = self else { - return - } - self.isUploading = false - switch result { - case let .success(checkResult): - self.imageChecked = true - //self.realateUrlAdpatEPalAddSlash(add: true) - let nsfw = checkResult ?? true - let imgOk = !nsfw - self.delegate?.imageCheck(self, result: imgOk) - block?(imgOk) - case let .failure(error): - switch error { - case let .serviceError(code, msg): - if code == .imageCheckFailed{ - self.imageChecked = true - self.imageCheckIsViolation = true - self.errorMsg = msg - self.delegate?.imageCheck(self, result: false) - }else{ - self.imageCheckIsViolation = false - self.isFailed = true - self.isDone = false - self.delegate?.imageCheck(self, result: false) - } - default: - break - } - block?(false) - } - } - } - - func contentType() -> String { - switch suffixEnum { - case .gif: return "image/gif" - case .png: return "image/png" - default: return "image/jpeg" - } - } - - func releaseS3Utility() { - if let utility = utility { - //AWSS3TransferUtility.removeS3TransferUtility(forKey: fileName) - utility.cancelAll() - } - } - - func setupPhotoOversizeLimmit() { - isOverSizeLimmit = true - } - - func setupErrorMsg(_ msg: String) { - errorMsg = msg - } - - // MARK: - Setter Overrides - override var uploadProgress: Float { - get { super.uploadProgress } - set { - super.uploadProgress = newValue - delegate?.uploadProgress(newValue) - } - } - - override var isDone: Bool { - get { super.isDone } - set { - super.isDone = newValue - delegate?.uploadDoneWith(self) -// delegate?.uploadDone() - } - } - - override var isFailed: Bool { - get { super.isFailed } - set { - super.isFailed = newValue - delegate?.uploadFailWith(self) -// delegate?.uploadFail() - } - } - - // MARK: - UploadPhotoCallBackProtocol - override func mm_uploadProgress(_ progress: Float) { - self.uploadProgress = progress - } - - override func mm_uploadFailed() { - releaseS3Utility() - isFailed = true - isUploading = false - } - - override func mm_uploadDone(_ remoteImageUrlString: String) { - releaseS3Utility() - self.remoteImageUrlString = remoteImageUrlString - isDone = true - - if isAutoCheckImage { - if !banCheckImageInDelegateMethod { - checkImageOK(nil) - } - } else { - isUploading = false - realateUrlAdpatEPalAddSlash(add: true) - } - } - - // MARK: - Helper - @discardableResult - private func realateUrlAdpatEPalAddSlash(add: Bool) -> String { - var url = remoteImageUrlString ?? "" - if add { - if !url.isEmpty && !url.hasPrefix("/") { - url = "/\(url)" - } - } else { - if !url.isEmpty && url.hasPrefix("/") { - url.removeFirst() - } - } - remoteImageUrlString = url - return url - } -} diff --git a/crush/Crush/Src/Models/User.swift b/crush/Crush/Src/Models/User.swift deleted file mode 100644 index 3ba879d..0000000 --- a/crush/Crush/Src/Models/User.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// User.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import CodableWrappers -import Foundation -import UIKit - -enum Sex: Int, Codable { - case male = 0 - case female = 1 - case noncomfirming = 2 - - var localizedText: String { - switch self { - case .male: - return NSLocalizedString("Male", comment: "Gender: Male") - case .female: - return NSLocalizedString("Female", comment: "Gender: Female") - case .noncomfirming: - return NSLocalizedString("Nonconforming", comment: "Gender: Non-binary/Nonconforming") - } - } - - var icon: UIImage { - switch self { - case .male: - return UIImage(named: "sex_male_flag")! - case .female: - return UIImage(named: "sex_female_flag")! - case .noncomfirming: - return UIImage(named: "sex_no_gender_flag")! - } - } -} - -@objcMembers class User: NSObject, Codable { - var nickname: String? -// @CustomCodingKey("id") - var userId: Int = 0 - var sex: Sex = .noncomfirming -// var age: Int = 0 - var token: String? - /// yyyy-mm-dd - var birthday: Int64? - /// 是否需要强制完善用户基础信息 - var cpUserInfo: Bool = false - /// 头像 - var headImage: String? - - var thirdEmail: String? - var thirdNickname: String? - var thirdType : ThirdType? - var idCard: String? - /// 是否是会员 - var isMember: Bool? - /// 已创建AI数量 - var createdAiCount: Int? - /// 可创建AI数量 - var canCreateAiCount: Int? - - func canCreateAIRole()-> Bool{ - guard let created = createdAiCount, let totalCount = canCreateAiCount else{return false} - return totalCount > created - } -} - -struct ThirdLoginResponse: Codable { - var token: String? -} - - -struct UserCreateCountResponse: Codable{ - /// 免费创作次数 - var freeNum: Int? - var usedFreeNum: Int? - - /// 会员赠送创作次数 - var memberNum: Int? - var usedMemberNum:Int? - - /// 购买创作次数 - var buyNum: Int? - var usedBuyNum: Int? -} diff --git a/crush/Crush/Src/Models/WalletModels.swift b/crush/Crush/Src/Models/WalletModels.swift deleted file mode 100644 index bcaa8e2..0000000 --- a/crush/Crush/Src/Models/WalletModels.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// WalletModels.swift -// Crush -// -// Created by Leon on 2025/9/10. -// - -import Foundation - -// MARK: - VIP -// 顶层结构 -struct MemberDetailOutput: Codable { - var userMemberInfo: UserSubscription? - var memberPrivList: [MemberPrivDict]? -} - -// 用户会员信息 -struct UserSubscription: Codable { - var id: Int? - /// Need to be "APPLE" - var platform: String? - var userId: Int? - var subscriptionId: String? - var productId: String? - var memberType: MemberType? - var priceType: PriceType? - var purchaseToken: String? - var refundTimeMs: Int? - /// 1758289340000 - var purchaseTime: Int? - var expTime: Int? - var autoRenewStatus: Bool? - var status: String? - var createTime: Int? - var editTime: Int? - var ip: String? - /// 是否已经提醒过。 - var remind: Bool? - - func getVIPValid() -> Bool{ - if let expTimeTimestamp = expTime{ - if Date().timeStamp < expTimeTimestamp{ - return true - } - } - return false - } -} - -// 会员类型 -enum MemberType: String, Codable { - case vip = "VIP" -} - -// 价格周期 -enum PriceType: String, Codable { - case month = "SUB_MONTH" - case season = "SUB_SEASON" - case year = "SUB_YEAR" -} - -// 会员特权 -struct MemberPrivDict: Codable { - var id: Int? - /// 会员特权标题 - var title: String? - /// 会员特权描述 - var desc: String? - var img: String? - var sort: Int? - var isDelete: Int? - var createTime: Int? - /// 会员特权code , ADD_CRUSH_COIN、ADD_CREATE_AI等~ - var code: String? -} - -// MARK: - Charge - -struct CoinRechargePreOrderResponse: Codable{ - var tradeNo: String? - -} - -// MARK: - Bill -struct BillListRequest: Codable{ - var page: RequestPageData? - var type: String? - - var startTime: Int? - var endTime: Int? -} - -struct BillListResponse:Codable{ - /// 总收入 - var incomeTotal: Int? - /// 总支出 - var outcomeTotal: Int? - - var pageList: ResponseContentPageData? -} - -/// 流水 -struct BillListInfo: Codable{ - var tradeNo: String? - var desUid: Int? - var status: String? - var time: Int? - var amount: Int? - var item: String? - var nickname: String? - var period: String? - var headImg: String? - var custId: String? - var bizNum: String? - var inOrOut: InOrOut? - var periodAmount: Int? - var id: Int? - /// Buff - var payment: String? - var platform: String? - /// 后续可能添加类型, 可能解析失败 - var bizType: String? - var message: String? - var giftId: Int? - var toWithdrawableIncomeTime: Int? - /// BALANCE. AWAITING_INCOME - var buffType: BuffType? - /// Jsonstring: like { "aiId":.xxx, "giftId" : 1, "num": 12} - var extend: String? - - func getExtendObj()-> BillListExtentObj?{ - if let extentString = extend { - return CodableHelper.decode(BillListExtentObj.self, from: extentString) - } - return nil - } - /* - { - "tradeNo" : "12025091718400465000014", - "desUid" : null, - "status" : "WITHDRAW_SUCCESS", - "time" : 1758105605000, - "amount" : 7000, - "item" : "Create AI Image", - "nickname" : null, - "period" : null, - "headImg" : null, - "custId" : null, - "bizNum" : "2025091718400464367010", - "inOrOut" : "OUT", - "periodAmount" : null, - "id" : 161, - "payment" : "Buff", - "platform" : "Balance", - "bizType" : "CREATE_AI_IMAGE", - "message" : null, - "giftId" : null, - "toWithdrawableIncomeTime" : null, - "buffType" : "BALANCE", - "extend" : null - } - */ -} - -struct BillListExtentObj: Codable{ - var aiId: Int? - var giftId: Int? - var num: Int? -} - -// MARK: - Enums - -enum BuffClassify: String, Codable { - case bCharge = "B_CHARGE" - case bPurchase = "B_PURCHASE" - case bRefund = "B_REFUND" - case bTip = "B_TIP" - case bGift = "B_GIFT" - case bEpalSubscription = "B_EPAL_SUBSCRIPTION" - case bLuckyDraw = "B_LUCKY_DRAW" - case bStore = "B_STORE" - case bEpalPrize = "B_EPAL_PRIZE" - case bOther = "B_OTHER" - case iWithdraw = "I_WITHDRAW" - case iServiceIncome = "I_SERVICE_INCOME" - case iRefund = "I_REFUND" - case iTipIncome = "I_TIP_INCOME" - case iGiftIncome = "I_GIFT_INCOME" - case iSubscriptionIncome = "I_SUBSCRIPTION_INCOME" - case iEpalReward = "I_EPAL_REWARD" - case iStoreReward = "I_STORE_REWARD" - case iOther = "I_OTHER" -} - -enum BizType: String, Codable { - case game = "GAME" - case charge = "CHARGE" - case sub = "SUB" - case withdraw = "WITHDRAW" - case refund = "REFUND" - case frozenBalance = "FROZEN_BALANCE" - case unfrozenBalance = "UNFROZEN_BALANCE" - case createAIImage = "CREATE_AI_IMAGE" - case textModel = "TEXT_MODEL" - case chatAssistant = "CHAT_ASSISTANT" - case sendVoice = "SEND_VOICE" - case voiceCall = "VOICE_CALL" - case imageUnlock = "IMAGE_UNLOCK" - case heartbeatPurchase = "HEARTBEAT_PURCHASE" - case gift = "GIFT" - case unlockAdmirers = "UNLOCK_ADMIRERS" - case newUserGift = "NEW_USER_GIFT" - case vipBuffGift = "VIP_BUFF_GIFT" - case subscribeMember = "SUBSCRIBE_MEMBER" - case SIGN_IN_GIFT = "SIGN_IN_GIFT" -} -enum InOrOut: String, Codable { - case `in` = "IN" - case out = "OUT" -} - -enum BuffType: String, Codable { - case balance = "BALANCE" - case withdrawableIncome = "WITHDRAWABLE_INCOME" - case awaitingIncome = "AWAITING_INCOME" - case refund = "REFUND" - case frozenIncome = "FROZEN_INCOME" - case frozenBalance = "FROZEN_BALANCE" -} - -enum BillStatus: String, Codable { - case created = "CREATED" - case rollBack = "ROLL_BACK" - case settled = "SETTLED" -} - -enum WithdrawStatus: String, Codable { - case inReview = "IN_REVIEW" - case reviewFail = "REVIEW_FAIL" - case withdrawIng = "WITHDRAW_ING" - case withdrawFail = "WITHDRAW_FAIL" - case withdrawSuccess = "WITHDRAW_SUCCESS" - case withdrawFailBack = "WITHDRAW_FAIL_BACK" -} diff --git a/crush/Crush/Src/Modules/Chat/AI/View/IMAudioFlagView.swift b/crush/Crush/Src/Modules/Chat/AI/View/IMAudioFlagView.swift deleted file mode 100644 index 4229723..0000000 --- a/crush/Crush/Src/Modules/Chat/AI/View/IMAudioFlagView.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// IMAudioFlagView.swift -// Crush -// -// Created by Leon on 2025/8/20. -// - -import UIKit -import Lottie - -/// icon + 时长 -class IMAudioFlagView: UIView{ - - var stackH: UIStackView! - - var iconContainer: UIView! - var iconStatic: UIImageView! - var lottieView: LottieAnimationView! - - var secondsLabel: CLLabel! - - var topButton: UIButton! - - var seconds: Int? - - // MARK: - SpeechModel Properties - private var path: String = "" - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = .c.csfn - cornerRadius = 8 - self.snp.makeConstraints { make in - make.height.equalTo(28) - } - - stackH = { - let v = UIStackView() - v.spacing = 8 - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(12) - make.trailing.equalToSuperview().offset(-12) - } - return v - }() - - iconContainer = { - let v = UIView() - stackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 12, height: 12)) - } - return v - }() - - iconStatic = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .play, size: CGSize(width: 12, height: 12), color: .white) - iconContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - lottieView = { - let animation = LottieAnimation.named("voice_white") - let voiceAnimation = LottieAnimationView(animation: animation) - - voiceAnimation.isUserInteractionEnabled = false - voiceAnimation.loopMode = .loop - voiceAnimation.animationSpeed = 1.4 - voiceAnimation.contentMode = .scaleAspectFit - voiceAnimation.isHidden = true - voiceAnimation.tintColor = .white - iconContainer.addSubview(voiceAnimation) - voiceAnimation.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 12, height: 12)) - } - voiceAnimation.isHidden = true - return voiceAnimation - }() - - secondsLabel = { - let label = CLLabel() - label.font = .t.tls - label.isHidden = true - stackH.addArrangedSubview(label) - return label - }() - - topButton = { - let v = UIButton() - v.touchAreaInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - secondsLabel.isHidden = false - secondsLabel.text = "4''" - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - SpeechModel State Management - func reloadState(with model: SpeechModel?) { - guard let speechModel = model else { - reset() - return - } - self.path = speechModel.path - speechModel.stateChangedBlock = { [weak self] updatedModel in - guard let self = self, self.path == updatedModel.path else { return } - self.reloadViews(model: updatedModel) - self.checkState(model: updatedModel) - } - - let duration = speechModel.audioDuration - if duration > 0 { - secondsLabel.isHidden = false - secondsLabel.text = duration.imAIaudioDurationString - } - - speechModel.progressChangedBlock = {[weak self] current, total in - self?.secondsLabel.isHidden = false - self?.secondsLabel.text = total.imAIaudioDurationString - } - - reloadViews(model: speechModel) - } - - // MARK: - Private Methods - private func reloadViews(model: SpeechModel) { - if model.loadState == .loading { - startLoading() - } else { - reset() - } - if model.playState == .playing { - startVoice() - } else { - reset() - } - } - - private func checkState(model: SpeechModel) { - guard model.loadState == .complete else { return } - - switch model.playState { - case .default: - if model.canAutoPlay { - SpeechManager.shared.startPlay(with: model) - } - case .complete, .failed: - SpeechManager.shared.stopPlay(with: model) - case .playing: - break - } - } - - // MARK: - Original Methods (Modified) - func reset(){ - lottieView.stop() - lottieView.isHidden = true - iconStatic.isHidden = false - } - - func startLoading(){ - lottieView.animation = LottieAnimation.named("single_ring") - lottieView.isHidden = false - iconStatic.isHidden = true - lottieView.play() - } - - func startVoice(){ - lottieView.animation = LottieAnimation.named("voice_white") - lottieView.isHidden = false - iconStatic.isHidden = true - lottieView.play() - } - - // MARK: - Deinit - deinit { - //print("♻️ IMAudioFlagView dealloc") - } -} diff --git a/crush/Crush/Src/Modules/Chat/Class/GiftCountChooseController.swift b/crush/Crush/Src/Modules/Chat/Class/GiftCountChooseController.swift deleted file mode 100644 index 705d341..0000000 --- a/crush/Crush/Src/Modules/Chat/Class/GiftCountChooseController.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// GiftCountChooseController.swift -// Crush -// -// Created by Leon on 2025/8/24. -// -import SnapKit -import UIKit - -// MARK: - GiftCountChooseCell - -class GiftCountChooseCell: UITableViewCell { - let countLabel: UILabel - let line: UIView - let selectedIcon : UIImageView - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - countLabel = UILabel() - line = UIView() - selectedIcon = UIImageView() - super.init(style: style, reuseIdentifier: reuseIdentifier) - - backgroundColor = .clear - contentView.backgroundColor = .clear - selectionStyle = .none - - // Setup countLabel - countLabel.textAlignment = .center - countLabel.font = .t.tnms - countLabel.textColor = .text - contentView.addSubview(countLabel) - countLabel.snp.makeConstraints { make in - //make.edges.equalToSuperview() - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(16) - } - - selectedIcon.image = UIImage(named: "checkmark_tick") - contentView.addSubview(selectedIcon) - selectedIcon.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - selectedIcon.isHidden = true - - // Setup line -// line.backgroundColor = .c.con -// contentView.addSubview(line) -// line.snp.makeConstraints { make in -// make.leading.equalToSuperview().offset(8) -// make.trailing.equalToSuperview().offset(-8) -// make.height.equalTo(1) -// make.bottom.equalToSuperview() -// } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - GiftCountChooseController - -class GiftCountChooseController: UIViewController { - private let sourceRect: CGRect - private let container: UIView - private let datas: [String] - private let bgImageView: UIImageView - private let tableView: UITableView - private static let cellId = "GiftCountCell" - private static let rowHeight: CGFloat = 48 - var selectedValue: String? - - var chooseBlock: ((String) -> Void)? - var popoverDismiss: (() -> Void)? - - init(sourceRect: CGRect, selectedValue: String? = nil) { - self.sourceRect = sourceRect - self.selectedValue = selectedValue - container = UIView() - container.layer.borderColor = UIColor.c.cpn.cgColor - container.layer.borderWidth = 2 - container.layer.cornerRadius = 16 - container.layer.masksToBounds = true - - datas = ["100", "50", "20", "10", "5", "3", "1"].reversed() - bgImageView = UIImageView() - tableView = UITableView(frame: .zero, style: .plain) - super.init(nibName: nil, bundle: nil) - modalPresentationStyle = .fullScreen - view.backgroundColor = .clear - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func close() { - popoverDismiss?() - view.removeFromSuperview() - removeFromParent() - } - - private func setupUI() { - // Background button for dismissal - let button = UIButton() - view.addSubview(button) - button.addTarget(self, action: #selector(close), for: .touchUpInside) - button.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - // Setup container - let w: CGFloat = 120// 76 - let h: CGFloat = 232 //Self.rowHeight * CGFloat(datas.count) + 8 - let x: CGFloat = sourceRect.origin.x - (w / 2) - let y: CGFloat = sourceRect.origin.y - 20 - h - 8 // 8: padding for arrow - container.frame = CGRect(x: x, y: y, width: w, height: h) - container.backgroundColor = .c.csfn - view.addSubview(container) - - // Setup table view - tableView.dataSource = self - tableView.delegate = self - tableView.rowHeight = Self.rowHeight - tableView.layer.cornerRadius = 8 - //tableView.isScrollEnabled = false - tableView.backgroundColor = .clear - tableView.separatorStyle = .none - tableView.tableFooterView = UIView() - tableView.register(GiftCountChooseCell.self, forCellReuseIdentifier: Self.cellId) - container.addSubview(tableView) - tableView.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 0, bottom: 8, right: 0)) - } - - // 延迟滚动到选中项 - DispatchQueue.main.async { - self.scrollToSelectedValue() - } - } - - private func scrollToSelectedValue() { - guard let selectedValue = selectedValue, - let selectedIndex = datas.firstIndex(of: selectedValue) else { - return - } - - let indexPath = IndexPath(row: selectedIndex, section: 0) - tableView.scrollToRow(at: indexPath, at: .middle, animated: false) - } - - - - deinit{ - dlog("♻️GiftCountChooseController") - } -} - -// MARK: - UITableViewDataSource, UITableViewDelegate - -extension GiftCountChooseController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return datas.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellId, for: indexPath) as! GiftCountChooseCell - let currentValue = datas[indexPath.row] - cell.countLabel.text = currentValue - cell.line.isHidden = indexPath.row == datas.count - 1 - - // 显示选中状态的图标 - cell.selectedIcon.isHidden = currentValue != selectedValue - - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: false) - let selectedValue = datas[indexPath.row] - self.selectedValue = selectedValue - chooseBlock?(selectedValue) - close() - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMAIMsgContentView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMAIMsgContentView.swift deleted file mode 100644 index 6410847..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMAIMsgContentView.swift +++ /dev/null @@ -1,225 +0,0 @@ -// -// IMAIMsgContentView.swift -// Crush -// -// Created by Leon on 2025/8/20. -// - -import UIKit -import ActiveLabel -class IMAIMsgContentConfig: IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { - guard model.v2msg != nil else { return .zero } - - let contentView = IMAIMsgContentView.init(frame: .zero) - let content = contentView.contentWith(model: model) - // Way 1 to calculate - //contentView.contentLabel.attributedText = contentView.formatAttrubuteString(string: content) - //var size = contentView.contentLabel.sizeThatFits(CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: CGFloat.greatestFiniteMagnitude)) - - // Way 2 to calculate✅ - let attributedString = contentView.formatAttrubuteString(string: content) - var size = attributedString.boundingRect( - with: CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: .greatestFiniteMagnitude), - options: [.usesLineFragmentOrigin, .usesFontLeading], - context: nil - ).size - let calculatedHeight = ceil(size.height) - size.height = calculatedHeight - - // dlog("最大宽度\(SessionBaseModel.maxBubbleContentWidth) 高度:\(size.height)") - if size.height < 20 { - size = CGSize(width: size.width, height: 20) - } - return size - } - - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 24, bottom: 8, right: 16) - } - - override func contentInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 36, left: 16, bottom: 16, right: 16) - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMAIMsgContentView.self - } -} - -class IMAIMsgContentView: IMContentBaseView{ - var effectView: UIVisualEffectView! - var contentLabel: LineSpaceLabel! // ActiveLabel - var audioView : IMAudioFlagView! - - lazy var audioHelper = IMAudioHelper() - required override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupUI() { - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - v.alpha = 1 - v.backgroundColor = .c.csedn - v.cornerRadius = 16 - insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 0, bottom: 0, right: 0)) - } - return v - }() - - contentLabel = { - //let v = ActiveLabel() - //v.font = CLSystemToken.font(token: .tbm) - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbm) - v.config(typo) - - v.textColor = UIColor.c.ctsn - v.numberOfLines = 0 - v.textColor = .white - - containerView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - audioView = { - let v = IMAudioFlagView() - v.topButton.addTarget(self, action: #selector(tapAudioButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalToSuperview().offset(4) // -12 - } - return v - }() - - contentLabel.text = "" - //contentLabel.textColor = .white - - // Long press gesture - let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:))) - addGestureRecognizer(longPressGesture) - } - - func contentWith(model: SessionBaseModel) -> String { - var content: String? = nil - let message = model.v2msg - - if String.realEmpty(str: content) { - content = message?.text - } - -// #warning("test") -// content = "(Watching her parents toast you respectfully, I feel very uncomfortable. After all, she has been standing on the top of the magic capital since she was a child. She has never seen her parents like this, but should I say that he is really handsome?) Are you?" - - // dlog("☁️content:\(String(describing: content)), createTime:\(String(describing: message?.createTime)), modifytime:\(String(describing: message?.modifyTime))") - - return content ?? "" - } - - func formatAttrubuteString(string: String) -> NSMutableAttributedString{ - let content = string - let basic = [NSAttributedString.Key.font: UIFont.t.tbm, - NSAttributedString.Key.foregroundColor: UIColor.white, - ] - let aStr = NSMutableAttributedString(string: content, attributes: basic) - - //content.withAttributes([.font(.t.tbm), .textColor(.text)]) - let ranges = String.findBracketRanges(in: content) - let att = [NSAttributedString.Key.foregroundColor: UIColor.c.ctsn] - - for range in ranges { - aStr.addAttributes(att, range: range) - } - return aStr - } - - override func refreshModel(model: SessionBaseModel) { - super.refreshModel(model: model) - - let content = contentWith(model:model) - - // 语速,预估语音时长 - var speedrate = 0 - if let userSpeed = IMAIViewModel.shared.aiIMInfo?.dialogueSpeechRate, let intSpeed = Int(userSpeed){ - speedrate = intSpeed - } - let duration = audioHelper.calculateAudioDuration(text: content, speechRate: speedrate) - audioView.secondsLabel.text = duration.imAIaudioDurationString - - contentLabel.attributedText = formatAttrubuteString(string: content) - - audioView.reloadState(with: model.speechModel) - - if model.autoPlayAudioOnce && model.autoPlayAlreadyPlayed == false{ - tapAudioButton() - model.autoPlayAlreadyPlayed = true - } - } - - // MARK: - Action - @objc private func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) { - if gesture.state == .began { - // dlog("Long press detected") - let event = IMEventModel() - event.eventType = .aiMsgLongPress - event.cellModel = self.model - event.senderView = self - delegate?.onTapAction(event: event) - } - } - - @objc private func tapAudioButton(){ - // let text = contentWith(model: self.model) - // Generate voice(mp3 base64string) - dlog("tap Audio button...") - - let event = IMEventModel() - event.eventType = .playAITextToAudio - event.cellModel = self.model - event.senderView = self - delegate?.onTapAction(event: event) - - audioView.startLoading() - } - - - // MARK: - Helper - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - // 先用系统默认的命中检测 - let view = super.hitTest(point, with: event) - if view != nil { - return view - } - - // 遍历子视图,专门处理 UIButton - for subview in subviews { - // 只处理 UIButton 类型 - guard subview is UIButton else { continue } - - // 把点击点转换到子视图坐标系 - let convertedPoint = subview.convert(point, from: self) - - // 如果落在按钮区域内,就返回按钮 - if subview.bounds.contains(convertedPoint) { - return subview.hitTest(convertedPoint, with: event) - } - } - - return nil - } -} - diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMAIMsgLoadingView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMAIMsgLoadingView.swift deleted file mode 100644 index a63fd02..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMAIMsgLoadingView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// IMAIMsgLoadingView.swift -// Crush -// -// Created by Leon on 2025/8/28. -// -import UIKit -import Lottie - -class IMAIMsgLoadingConfig : IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { - return CGSize(width: 20, height: 20) - } - - - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 16, left: 24, bottom: 16, right: 24) - } - - override func contentInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 16, left: 24, bottom: 16, right: 24) - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMAIMsgLoadingView.self - } -} - -class IMAIMsgLoadingView :IMContentBaseView{ - var block: UIView! - var lottie: LottieAnimationView! - - required override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupUI() { - block = { - let v = UIView() - v.backgroundColor = .c.csedn - v.cornerRadius = 16 - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - lottie = { - let animation = LottieAnimation.named("three_dots_msg_loading") - let animationView = LottieAnimationView(animation: animation) - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - animationView.size = CGSize(width: 20, height: 20) - block.addSubview(animationView) - animationView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 20, height: 20)) - } - return animationView - }() - - lottie.play() - } - - deinit{ - lottie.stop() - - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMContentBaseConfig.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMContentBaseConfig.swift deleted file mode 100644 index f00e254..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMContentBaseConfig.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// IMContentBaseConfig.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -class IMContentBaseConfig:NSObject, SessionCellContentDelegate{ - required override init() { - super.init() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func contentSize(model: SessionBaseModel) -> CGSize { - return .zero - } - - func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) - } - - func contentInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } - - func onlyShowContent(model: SessionBaseModel) -> Bool { - return false - } - - func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMContentBaseView.self - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMContentBaseView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMContentBaseView.swift deleted file mode 100644 index b5e3e5f..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMContentBaseView.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// IMContentBaseView.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import UIKit - -protocol IMContentViewDelegate: NSObjectProtocol{ - func onTapAction(event: IMEventModel) -} - -class IMContentBaseView: UIView{ - var model: SessionBaseModel! - var containerView: UIView = UIView() - - weak var delegate: IMContentViewDelegate? - - /// 刷新数据, 子类必须实现 - public func refreshModel(model: SessionBaseModel) { - self.model = model - // 更新约束 - let insets = model.config.contentInsets(model: model) - self.containerView.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(insets.left) - make.trailing.equalToSuperview().offset(-insets.right) - make.top.equalToSuperview().offset(insets.top) - make.bottom.equalToSuperview().offset(-insets.bottom) - } - self.layoutIfNeeded() - } - - override init(frame: CGRect) { - super.init(frame: frame) - - self.addSubview(self.containerView) - // self.containerView.layer.cornerRadius = 16 // 不用cell的maskLayer - // 容器 - self.containerView.snp.makeConstraints { make in - make.center.equalTo(self) - } - - let tap = UITapGestureRecognizer.init(target: self, action: #selector(cellTapAction)) - tap.cancelsTouchesInView = false - self.addGestureRecognizer(tap) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc func cellTapAction() { - // dlog("IMContentBaseView cellTapAction") - } - -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMGiftContentView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMGiftContentView.swift deleted file mode 100755 index 857b46c..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMGiftContentView.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// IMGiftContentView.swift -// LegendTeam -// -// Created by liangbo on 2022/3/2. -// - -import UIKit - -class IMGiftContentConfig: IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { - return CGSize(width:SessionBaseModel.maxBubbleContentWidth , height: 72) - } - - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 1, left: 16, bottom: 8, right: 16) - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMGiftContentView.self - } -} - -class IMGiftContentView: IMContentBaseView { - - var effectView: UIVisualEffectView! - var iconBlock: UIView! - var giftIcon: UIImageView! - var label: LineSpaceLabel! - - required override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupUI() { - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 1 - v.cornerRadius = 16 - insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - iconBlock = { - let v = UIView() - v.cornerRadius = 8 - v.backgroundColor = .c.cseln - containerView.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 48, height: 48)) - make.leading.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - } - return v - }() - - giftIcon = { - let v = UIImageView() - iconBlock.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo((CGSize(width: 48, height: 48))) - make.center.equalToSuperview() - } - return v - }() - - label = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbm) - v.config(typo) - containerView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(iconBlock.snp.trailing).offset(10) - make.trailing.lessThanOrEqualToSuperview().offset(-10) - } - return v - }() - } - - override func refreshModel(model: SessionBaseModel) { - super.refreshModel(model: model) - - - guard let info = IMCustomAttachment.getCustomInfo(model: model) else{ - return - } - - giftIcon.loadImage(info.giftIcon, bgColor: .clear) - label.text = "\(info.giftName ?? "-") x\(info.giftNum ?? 1)"//"Diamod x1" - } - - @objc func checkTapAction() { - // 跳转钱包 - AppRouter.goWalletCenter() - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMImageContentView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMImageContentView.swift deleted file mode 100644 index 38236cb..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMImageContentView.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// IMImageContentView.swift -// Crush -// -// Created by Leon on 2025/8/22. -// - -import UIKit -class IMImageContentConfig: IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { -// return CGSize(width: 60, height: 60) - guard let attach = model.baseRemoteInfo?.customAttachment else { return CGSize(width: 144, height: 144) } - - var imgSize = CGSize(width: 1920, height: 1080) - if let price = attach.unlockPrice, price > 0{ - return CGSize(width: 144, height: 144) - }else{ - imgSize = CGSize(width: attach.width?.value ?? 1920, height: attach.height?.value ?? 1080) - } - - let constants = ImageDisplayLogic.adjustConstants(for: SessionBaseModel.maxBubbleContentWidth) - let sizeInfo = ImageDisplayLogic.calculateImageDisplaySize( - originalWidth: imgSize.width, - originalHeight: imgSize.height, - constants: constants - ) - //dlog("🐰type:\(sizeInfo.type)") - return CGSize(width: sizeInfo.width, height: sizeInfo.height) - } - - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 8, left: 24, bottom: 8, right: 24) - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMImageContentView.self - } -} - -class IMImageContentView: IMContentBaseView { - var effectView: UIVisualEffectView! - var imageView: UIImageView! - - var unlockContainer: UIView! - var lockIcon : UIImageView! - var unlockIconPriceLabel: CLIconLabel! - - override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupUI() { - - containerView.cornerRadius = 16 - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - v.alpha = 1 - v.cornerRadius = 16 - insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - imageView = UIImageView() - containerView.addSubview(imageView) - imageView.contentMode = .scaleAspectFill - imageView.snp.makeConstraints { make in - make.edges.equalTo(self) - } - - unlockContainer = { - let v = UIView() - containerView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - let lockIcon = UIImageView() - lockIcon.image = MWIconFont.image(fromIcon: .iconPrivate, size: CGSize(width: 32, height: 32), color: .white) - v.addSubview(lockIcon) - lockIcon.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalToSuperview().offset(46) - } - self.lockIcon = lockIcon - - v.isHidden = true - - return v - }() - - unlockIconPriceLabel = { - let v = CLIconLabel() - v.iconSize = CGSize(width: 16, height: 16) - v.iconImageView.image = UIImage(named: "icon_32_diamond") - v.contentLabel.font = .t.tlm - unlockContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(lockIcon.snp.bottom).offset(16) - } - return v - }() - } - - override func refreshModel(model: SessionBaseModel) { - super.refreshModel(model: model) - -// guard let raw = model.v2msg?.attachment?.raw, let data = CodableHelper.decode(IMCustomAttachment.self, from: raw) else{ -// return -// } - - guard let customAttachment = model.baseRemoteInfo?.customAttachment else{ - return - } - - if model.baseRemoteInfo?.cellType != .image{ - assert(false) - return - } - - // model.v2msg?.attachment - imageView.loadImage(customAttachment.url) - - if let unlockPrice = customAttachment.unlockPrice, unlockPrice > 0{ - unlockContainer.isHidden = false - unlockIconPriceLabel.contentLabel.text = "\(String.thousandString(float: (Double(unlockPrice) / 100.0), maxDigits: 2)) unlock" - }else{ - unlockContainer.isHidden = true - } - - - - } - - override func cellTapAction() { - let event = IMEventModel() - event.eventType = .image - event.cellModel = self.model - event.senderView = self.imageView - delegate?.onTapAction(event: event) - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMPhoneCallContentView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMPhoneCallContentView.swift deleted file mode 100755 index 7a085f1..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMPhoneCallContentView.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// IMPhoneCallContentView.swift -// LegendTeam -// -// Created by dong on 2022/4/20. -// - -import UIKit - -class IMPhoneCallConfig: IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { - guard let message = model.v2msg else { return .zero } - - let contentView = IMPhoneCallContentView(frame: .zero) - contentView.contentLabel.text = contentView.getContentTextWith(message: message) - var size = contentView.contentLabel.sizeThatFits(CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: CGFloat.greatestFiniteMagnitude)) - if size.height < 28 { - size = CGSize(width: size.width, height: 28) - } - size = CGSize(width: size.width + 44, height: size.height) - return size - } - -// override func contentInsets(model: SessionBaseModel) -> UIEdgeInsets { -// return UIEdgeInsets(top: 8.5, left: 20, bottom: 8.5, right: 20) -// } - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMPhoneCallContentView.self - } - - override func onlyShowContent(model: SessionBaseModel) -> Bool { - return true - } -} - -class IMPhoneCallContentView: IMContentBaseView { - var effectView: UIVisualEffectView! - var contentLabel: UILabel! - var phontIcon: UIImageView! - - override init(frame: CGRect) { - super.init(frame: frame) - initialViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func initialViews() { - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 1 - v.cornerRadius = 4 - insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - contentLabel = UILabel() - self.containerView.addSubview(contentLabel) - contentLabel.font = .t.tbs - contentLabel.textColor = .white - contentLabel.numberOfLines = 0 - contentLabel.textAlignment = .center - contentLabel.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 28)) - } - - phontIcon = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .iconCallHangup, size: CGSize(width: 16, height: 16), color: .white) - self.containerView.addSubview(v) - v.snp.makeConstraints { make in - //make.leading.equalTo(contentLabel.snp.trailing).offset(8) - make.trailing.equalToSuperview().offset(-8) - make.centerY.equalToSuperview() - } - return v - }() - } - - override func refreshModel(model: SessionBaseModel) { - super.refreshModel(model: model) - - guard let nimMessage = model.v2msg else { - return - } - - contentLabel.text = getContentTextWith(message: nimMessage) - - layoutIfNeeded() - UIView.setAnimationsEnabled(true) - } - - fileprivate func getContentTextWith(message: V2NIMMessage) -> String { - let info = IMRemoteUtil.dealRemoteInfo(message: message) - return info.displayString ?? "" - } - - override func cellTapAction() { - let event = IMEventModel() - event.eventType = .phonecallTap - event.cellModel = model - delegate?.onTapAction(event: event) - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMTextContentView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMTextContentView.swift deleted file mode 100644 index b674a7c..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMTextContentView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// IMTextContentView.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import UIKit -import ActiveLabel -class IMTextContentConfig: IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { - guard model.v2msg != nil else { return .zero } - - let contentView = IMTextContentView.init(frame: .zero) -// contentView.contentLabel.text = contentView.contentWith(model: model) - //contentView.contentLabel.nim_setText(contentView.contentWith(model: model)) -// var size = contentView.contentLabel.sizeThatFits(CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: CGFloat.greatestFiniteMagnitude)) - - let string = contentView.contentWith(model: model) ?? " " - var size = string.size(for: UIFont.t.tbm, size: CGSize(width: SessionBaseModel.maxBubbleContentWidth, height: CGFloat.greatestFiniteMagnitude), lineBreakMode: .byWordWrapping) - var fixSize = CGSize(width: size.width + 2, height: size.height) - // dlog("size:\(fixSize)") - if fixSize.height < 20 { - fixSize = CGSize(width: size.width, height: 20) - } - return fixSize - } - - override func contentInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 20, left: 24, bottom: 20, right: 24) - } - - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMTextContentView.self - } -} - -class IMTextContentView: IMContentBaseView{ - var contentLabel: ActiveLabel! - - required override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupUI() { - contentLabel = { - let v = ActiveLabel() - v.textColor = UIColor.c.ctsn - v.font = CLSystemToken.font(token: .tbm) - v.numberOfLines = 0 - //v.enabledTypes = [.url] - v.textColor = .white - - containerView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - } - - func contentWith(model: SessionBaseModel) -> String! { - var content: String? = nil - let message = model.v2msg - - if String.realEmpty(str: content) { - content = message?.text - } - return content ?? "" - } - - override func refreshModel(model: SessionBaseModel) { - super.refreshModel(model: model) - - contentLabel.text = "" - - if model.shouldShowLeft { - contentLabel.textColor = .white - } else { - contentLabel.textColor = .white - } - contentLabel.text = contentWith(model:model) - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMTimeStampContentView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMTimeStampContentView.swift deleted file mode 100755 index 490123b..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMTimeStampContentView.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// IMTimeStampContentView.swift -// LegendTeam -// -// Created by 梁博 on 21/12/21. -// - -import UIKit - -class IMTimeStampContentConfig: IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { - //guard let message = model.v2msg else { return .zero } - - let contentView = IMTimeStampContentView.init(frame: .zero) - contentView.contentLabel.text = contentView.contentTextWith(model: model) - let size = contentView.contentLabel.sizeThatFits(CGSize(width: SessionBaseModel.maxContentWidth, height: CGFloat.greatestFiniteMagnitude)) - let width = size.width - - return CGSize(width: width + 16, height: 28) - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMTimeStampContentView.self - } - - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) - } - - override func onlyShowContent(model: SessionBaseModel) -> Bool { - return true - } -} - -class IMTimeStampContentView: IMContentBaseView { - var effectView: UIVisualEffectView! - var contentLabel: UILabel! - - override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupUI() { - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 1 - v.cornerRadius = 4 - insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - contentLabel = UILabel() - self.containerView.addSubview(contentLabel) - contentLabel.font = .t.tbs - contentLabel.textColor = .white - contentLabel.numberOfLines = 0 - contentLabel.textAlignment = .center - contentLabel.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)) - } - } - - func contentTextWith(model: SessionBaseModel?) -> String { - var content = "" - - let second = Int(model?.timeStamp ?? 0) - content = Date.timerStyle(style: .IMCHAT, second: second) - return content - } - - override func refreshModel(model: SessionBaseModel) { - super.refreshModel(model: model) - - contentLabel.text = contentTextWith(model: model) - } -} diff --git a/crush/Crush/Src/Modules/Chat/ContentView/IMTipsContentView.swift b/crush/Crush/Src/Modules/Chat/ContentView/IMTipsContentView.swift deleted file mode 100755 index 29bf51b..0000000 --- a/crush/Crush/Src/Modules/Chat/ContentView/IMTipsContentView.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// IMTipsContentView.swift -// LegendTeam -// -// Created by 梁博 on 21/12/21. -// - -import UIKit - -class IMTipsContentConfig: IMContentBaseConfig { - override func contentSize(model: SessionBaseModel) -> CGSize { - guard let message = model.v2msg else { return .zero } - - let contentView = IMTipsContentView.init(frame: .zero) - contentView.contentLabel.text = contentView.contentWith(message: message) - let size = contentView.contentLabel.sizeThatFits(CGSize(width: SessionBaseModel.maxContentWidth, height: CGFloat.greatestFiniteMagnitude)) - var width = size.width - if size.height > 30 { - width = SessionBaseModel.maxContentWidth - } - return CGSize(width: width + 16, height: size.height + 8 + 1)// +8 - } - - override func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type { - return IMTipsContentView.self - } - - override func cellInsets(model: SessionBaseModel) -> UIEdgeInsets { - return UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) - } - - override func onlyShowContent(model: SessionBaseModel) -> Bool { - return true - } -} - -class IMTipsContentView: IMContentBaseView { - var effectView: UIVisualEffectView! - var contentLabel: LineSpaceLabel! - - - required override init(frame: CGRect) { - super.init(frame: frame) - - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupUI() { - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 1 - v.cornerRadius = 4 - insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - contentLabel = LineSpaceLabel() - addSubview(contentLabel) - contentLabel.textAlignment = .center - contentLabel.numberOfLines = 0 - contentLabel.textColor = .white - let typo = CLSystemToken.typography(token: .tbs) - contentLabel.config(typo) - contentLabel.backgroundColor = .clear - contentLabel.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)) - } - } - - func contentWith(message: V2NIMMessage?) -> String! { - var content: String? = nil -// let info = IMRemoteUtil.dealRemoteInfo(message: message) -// let remote = message?.remoteExt as? [String: Any] -// if info.cellType == .create { -// let createInfo = IMCreateRemoteInfo.deserialize(from: remote, designatedPath: nil) ?? IMCreateRemoteInfo() -// content = createInfo.getContent() -// } else if (info.cellType == .keyword) { -// var hasKeyword = false -// if let yidunExt = message?.yidunAntiSpamRes { -// let yidunInfo = IMYidunExtInfo.deserialize(from: yidunExt, designatedPath: nil) -// if yidunInfo?.code == 200 { -// hasKeyword = true -// } -// } -// if hasKeyword { -// content = IMTipsContentView.yidunContent(message: message) -// } else { -// let createInfo = IMKewordRemoteInfo.deserialize(from: remote, designatedPath: nil) ?? IMKewordRemoteInfo() -// content = createInfo.getContent() -// } -// -// } - if content == nil { - content = message?.text - } - return content ?? "" - } - - override func refreshModel(model: SessionBaseModel) { - super.refreshModel(model: model) - let content = contentWith(message: model.v2msg) - contentLabel.text = content - if content?.count ?? 0 > 60 { - contentLabel.textAlignment = .left - } else { - contentLabel.textAlignment = .center - } - } - -// static public func yidunContent(message:NIMMessage?) -> String { -// guard let yidunExt = message?.yidunAntiSpamRes else { return "" } -// guard let yidunInfo = IMYidunExtInfo.deserialize(from: yidunExt, designatedPath: nil) else { return "" } -// guard let antInfo = IMYidunAntispamInfo.deserialize(from: yidunInfo.ext, designatedPath: "antispam") else { return "" } -// guard let labels = antInfo.labels else { return "" } -// var values = [String]() -// for label in labels { -// if let subLabels = label.subLabels { -// for subInfo in subLabels { -// if let detailInfo = subInfo.details { -// if let hitInfos = detailInfo.hitInfos { -// for hitInfo in hitInfos { -// if let value = hitInfo.value, value.count > 0 { -// values.append(value) -// } -// -// } -// } -// } -// } -// } -// } -// -// if values.count > 0 { -// let keyword = values.joined(separator: "/") -// return R.string.localizable.contain_sensitive_words.localized([keyword]) -// } else { -// return R.string.localizable.contain_sensitive_words.localized() -// } -// } -} diff --git a/crush/Crush/Src/Modules/Chat/Model/IMBaseRemoteInfo.swift b/crush/Crush/Src/Modules/Chat/Model/IMBaseRemoteInfo.swift deleted file mode 100644 index 9c46754..0000000 --- a/crush/Crush/Src/Modules/Chat/Model/IMBaseRemoteInfo.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// IMBaseRemoteInfo.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import UIKit - -// 各类数据模型 -enum MsgOptType: Int, Codable { - case unknow = 0 - case upvote = 1 - case downvote = 2 - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let rawValue = try? container.decode(Int.self) - self = MsgOptType(rawValue: rawValue ?? 0) ?? .unknow - } -} - -/** - serverExtension 和 customAttachment的后端数据 - */ -class IMBaseRemoteInfo: Codable{ - - var cellType: SessionCellType! = .text - /// 显示用的text - var displayString: String? - - var type: String? - - /// 消息点赞或踩 - var optType : MsgOptType? - - /// 消息评分(在消息扩展字段) - var score: CGFloat? - - /// 只在自定义消息中存在 - var customAttachment: IMCustomAttachment? - - /// 心动分值(String) in Conversation的serverExtension - var heartbeatVal: CGFloat? - /// 心动等级 in Conversation的serverExtension - var heartbeatLevel: HeartbeatLevel? - /// 关系展示开关 in Conversation serverExtension - var isShow: Bool? - - /// 通过 SessionBaseModel 的数据,快捷获取对应的 后端数据IMBaseRemoteInfo - static func getBaseInfo(model: SessionBaseModel) -> IMBaseRemoteInfo { - var baseInfo: IMBaseRemoteInfo! - if model.baseRemoteInfo != nil { - baseInfo = model.baseRemoteInfo - } else { - baseInfo = IMRemoteUtil.dealRemoteInfo(message: model.v2msg) - model.baseRemoteInfo = baseInfo - } - return baseInfo - } -} - - -class IMYidunExtInfo: Codable { - var ext: String? - var priceUnitDesc: String? - var type: String? - var version: String? - var taskId: String? - - var code: Int? - var suggestion: Int? - var status: Int? - -} diff --git a/crush/Crush/Src/Modules/Chat/Model/IMChatSetting.swift b/crush/Crush/Src/Modules/Chat/Model/IMChatSetting.swift deleted file mode 100644 index fe79b5e..0000000 --- a/crush/Crush/Src/Modules/Chat/Model/IMChatSetting.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// IMChatSetting.swift -// Crush -// -// Created by Leon on 2025/8/26. -// - -struct IMChatSetting: Codable { - var aiId: Int? // ai的Id - var nickname: String? // 昵称 - var sex: Sex? // 0,男;1,女;2,自定义 - var birthday: Int64? // 出生日期,时间戳格式 - var whoAmI: String? // 我是谁 - var modelCode: String? // 对话模型code - var modelName: String? // 对话模型名称 - /// 聊天气泡code,CB0001 - var bubbleCode: String? - var bubbleName: String? // 聊天气泡名称 - var backgroundImg: String? // 聊天背景图片 - var isAutoPlayVoice: Int? // 自动播放语音开关 1:开 0:关 - /// 是否是默认背景图 - var isDefaultBackground: Bool? -} - -// MARK: - 解锁类型 -enum UnlockType: String, Codable { - case member = "MEMBER" // 会员 - case heartbeatLevel = "HEARTBEAT_LEVEL" // 心动等级 -} -// MARK: - 聊天气泡 -struct IMChatBubble: Codable{ - var id: Int? // id - var code: String? // code - var name: String? // 名称 - var color: String? // 颜色 ? - /// Android use - var imgUrl: String? // 图片url - /// ⬇️ iOS Use - var webImgUrl: String? - var unlockType: UnlockType? // 解锁类型 - var unlockHeartbeatLevel: HeartbeatLevel? // 解锁心动等级 - var isUnlock: Bool? - /// 是否默认 - var isDefault: Bool? - - func canUseTheBubble()->Bool{ - return isUnlock.boolValue || isDefault.boolValue - } -} - -struct IMChatBackground: Codable{ - var backgroundId: Int? // 背景图片id - var imgUrl: String? // 图片地址 - /// "720" - var width: String? // 图片宽 - /// "1024" - var height: String? // 图片高 - var isDefault: Bool? // 是否默认图片 - var isSelected: Bool? // 是否选中图片 -} diff --git a/crush/Crush/Src/Modules/Chat/Model/IMConfigInfo.swift b/crush/Crush/Src/Modules/Chat/Model/IMConfigInfo.swift deleted file mode 100644 index c2b56c2..0000000 --- a/crush/Crush/Src/Modules/Chat/Model/IMConfigInfo.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// IMConfigInfo.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import UIKit - -class IMConfigInfo: Codable{ - /// 2d6abc076f434fc52320c7118de5fead - // var appKey: String? - var accountId: String? - var token: String? - //var customerServiceAccId: String? -} diff --git a/crush/Crush/Src/Modules/Chat/Model/IMEventModel.swift b/crush/Crush/Src/Modules/Chat/Model/IMEventModel.swift deleted file mode 100644 index e72b28b..0000000 --- a/crush/Crush/Src/Modules/Chat/Model/IMEventModel.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// IMEventModel.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import Foundation - -enum IMEventType { - case unknown - case contentUrl - case messageLimit - case image - case contactUS - case phonecallTap - case aiMsgLongPress - /// 播放ai文字->语音 - case playAITextToAudio -} - -class IMEventModel{ - var eventType:IMEventType = .unknown - - var cellModel: SessionBaseModel? - - var linkData: String? - - weak var senderView : UIView? -} diff --git a/crush/Crush/Src/Modules/Chat/Model/IMMessageModels.swift b/crush/Crush/Src/Modules/Chat/Model/IMMessageModels.swift deleted file mode 100644 index 72c8d10..0000000 --- a/crush/Crush/Src/Modules/Chat/Model/IMMessageModels.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// IMMessageModels.swift -// Crush -// -// Created by Leon on 2025/8/22. -// - -import Foundation - -enum CLCustomAttchType: String, Codable { - case IMAGE - case IM_SEND_GIFT - case HEARTBEAT_LEVEL_UP - case HEARTBEAT_LEVEL_DOWN - case CALL - // 余额不足 raw:{"type":"INSUFFICIENT_BALANCE","content":"余额不足"} - case INSUFFICIENT_BALANCE - /// 过滤此消息, 后端用来保持语音和文本之间的上下文使用 - case VOICE_CHAT_CONTENT - - /// 一些自定义消息需要滑动显示完成 - var needInstantScrollDisplay: Bool{ - switch self { - case .IMAGE, .IM_SEND_GIFT: - return true - default: - return false - } - } -} - -struct IMCustomAttachment: Codable{ - var type: CLCustomAttchType? - /// 图片url - var url: String? // 可能是模糊图片 - var width: FlexibleCGFloat? - var height: FlexibleCGFloat? - var albumId: Int? // 相册id - var unlockPrice: Int? // AI图片解锁需要的价格。 - - // - 礼物专属 - var giftId: Int? - var giftName: String? - var giftNum: Int? - var giftIcon: String? - /** - // HEARTBEAT_LEVEL_UP 和 HEARTBEAT_LEVEL_DOWN的title - imContent - */ - var title : String? - var icon: String? - - // - Call - var callType: CallType? - var duration: Int? - - // - 心动等级升降 - var heartbeatLevel: HeartbeatLevel? - var heartbeatLevelNum: Int? - var heartbeatVal: CGFloat? - var heartbeatLevelName: String? - - func getDisplayString() -> String{ - var displayString = "" -// guard let theType = type else{ -// return displayString -// } - - switch type { - case .IMAGE: - displayString = "[Image]" - case .IM_SEND_GIFT: - displayString = "[Gift]" - case .CALL: - displayString = dealCallAttachment() - default: - displayString = " " - #if DEBUG - // 最后一条消息,可能是自定义消息,且不含内容,比如coin不足。 - displayString = "[测试环境显示]Check 类型⚠️ is \(String(describing: type))" - #endif - } - return displayString - } - -} - -// MARK: - Helper -extension IMCustomAttachment{ - // ⚠️不清晰,需要换! - static func getCustomInfo(model: SessionBaseModel) -> IMCustomAttachment? { - guard let raw = model.v2msg?.attachment?.raw else{ - dlog("☁️❌ getCustom error") - return nil - } - return CodableHelper.decode(IMCustomAttachment.self, from: raw) - } - - private func dealCallAttachment() -> String { - guard let theCallType = callType else { return "" } - if theCallType == .CALL_CANCEL { - return "Call Canceled" - } else { - let durationInMinistamp = duration ?? 0 - return formatDuration(durationInMinistamp) - } - } - - fileprivate func formatDuration(_ duration: Int) -> String { - // 毫秒 → 秒(向上取整) - let totalSeconds = Int(ceil(Double(duration) / 1000.0)) - - let hours = totalSeconds / 3600 - let minutes = (totalSeconds / 60) % 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%@ %02d:%02d:%02d", "Call Duration", hours, minutes, seconds) - } else { - return String(format: "%@ %02d:%02d", "Call Duration", minutes, seconds) - } - } -} - - -enum CallType: String, Codable{ - case CALL_CANCEL - case CALL_END -} diff --git a/crush/Crush/Src/Modules/Chat/Model/SessionBaseModel.swift b/crush/Crush/Src/Modules/Chat/Model/SessionBaseModel.swift deleted file mode 100644 index 6860a81..0000000 --- a/crush/Crush/Src/Modules/Chat/Model/SessionBaseModel.swift +++ /dev/null @@ -1,187 +0,0 @@ -// -// SessionBaseModel.swift -// Crush -// -// Created by Leon on 2025/8/18. -// -import UIKit -import Foundation -import NIMSDK -protocol SessionCellContentDelegate { - /// contentView 高度 - func contentSize(model: SessionBaseModel) -> CGSize - /// 气泡在cell中的边距(外) - func cellInsets(model: SessionBaseModel) -> UIEdgeInsets - /// contentView距离在气泡内距离四周的边距(bubble内) - func contentInsets(model: SessionBaseModel) -> UIEdgeInsets - /// 是否只显示contentView, 默认false。 true时,局中显示 - func onlyShowContent(model: SessionBaseModel) -> Bool - /// 获取当前conten View的类名 - func contentViewClass(model: SessionBaseModel) -> IMContentBaseView.Type -} - -enum SessionCellType: String, Codable { - case unknown - case text // 普通文本消息 - case aimsg // AI Msg - case image // 图片 - case timeStamp // 时间戳 - case create // 创建关系 - case keyword // 敏感词 - case tips // 提示 - case gift // 送礼 - case phonecall // im phone call - case aiLoading -} - -class SessionBaseModel: CustomStringConvertible, CustomDebugStringConvertible{ - /// 有气泡时,content 最大宽度 - static let maxBubbleContentWidth = floor(UIScreen.width * 0.65) - /// 无气泡时,content 最大宽度 - static let maxContentWidth = UIScreen.width - 32 - - var cellType: SessionCellType = .unknown - - /// Seconds timestam; - var timeStamp: TimeInterval = 0 - - var v2msg: V2NIMMessage? - - /// 根据message创建的数据缓存 - var baseRemoteInfo: IMBaseRemoteInfo? - - /// 是否是派生出来的model, 一般是一条消息显示两个cell中的卡片cell,本地追加创建的那种 默认NO - var isDeriveModel = false - /// 是否显示为卡片view - var shouldShowCardView = false - /// 是否显示为tips消息 - var shouldShowTips = false - /// 是否靠左显示,根据消息发送方来确定 - var shouldShowLeft = false - /// 默认YES,会显示气泡 - var shouldShowBubble = true - - /// 缓存高度 - var cacheCellHeight: CGFloat = 0 - /// content配置类 - var config: IMContentBaseConfig! - - var speechModel : SpeechModel? - - var autoPlayAudioOnce: Bool = false - var autoPlayAlreadyPlayed : Bool = false // 已经播放过了 - - /// 清空高度缓存 - public func clearCacheData() { - cacheCellHeight = 0 - } - - public func cellHeight() -> CGFloat { - if cacheCellHeight > 0 { - return cacheCellHeight - } - // 获取content的间距 - let size = config.contentSize(model: self) - let cellInsets = config.cellInsets(model: self) - let contentInsets = config.contentInsets(model: self) - // 计算高度 - let height: CGFloat = size.height + cellInsets.top + cellInsets.bottom + contentInsets.top + contentInsets.bottom - cacheCellHeight = height - return height - } - - // MARK: Audio About - - func prepareAudio(info: IMAIUserInfo?, completion:((_ model: SpeechModel?)-> Void)?){ - guard cellType == .aimsg, let text = v2msg?.text, text.count > 0, let voiceType = info?.voiceType else{ - return - } - - guard AudioPlayTool.audioChannelFreeToUse() else{ - dlog("Audio Channel 不可用") - return - } - - if let path = self.speechModel?.path, path.count > 0 { - completion?(self.speechModel) - return - } - - let saywords = String.removeBracketContents(from: text) - guard saywords.count > 0 else{ - return - } - - let speechRate = info?.dialogueSpeechRate ?? "0" - let loudnessRate = info?.dialoguePitch ?? "0" - - var params = [String: Any]() - params.updateValue(saywords, forKey: "text") - params.updateValue(voiceType, forKey: "voiceType") - params.updateValue(speechRate, forKey: "speechRate") - params.updateValue(loudnessRate, forKey: "pitchRate") - if let aiId = IMAIViewModel.shared.aiIMInfo?.aiId{ - params.updateValue(aiId, forKey: "aiId") - } - - AICowProvider.request(.voiceTts(params: params), modelType: String.self) {[weak self] result in - switch result { - case .success(let model): - if let audioString = model{ - self?.speechModel = SpeechModel.modelWithBase64String(audioString) - //self?.speechModel?.refreshPath(path: audioString) - completion?(self?.speechModel) - } - case .failure: - completion?(nil) - } - } - } - - - // MARK: - Static - // MARK: Equatable - static func == (lhs: SessionBaseModel, rhs: SessionBaseModel) -> Bool { - return lhs === rhs - } - /// 获取不同配置类 - public static func getContentConfigClass(type: SessionCellType) -> IMContentBaseConfig.Type { - switch type { - case .unknown: - return IMContentBaseConfig.self - case .text: - return IMTextContentConfig.self - case .aimsg: - return IMAIMsgContentConfig.self - case .timeStamp: - return IMTimeStampContentConfig.self - case .image: - return IMImageContentConfig.self - case .tips, .create, .keyword: - return IMTipsContentConfig.self - case .gift: - return IMGiftContentConfig.self - case .phonecall: - return IMPhoneCallConfig.self - case .aiLoading: - return IMAIMsgLoadingConfig.self -// default: -// return IMContentBaseConfig.self - } - } - - // MARK: - Other Debug - var description: String { - var attchmentString = "" - if let attchmentRaw = v2msg?.attachment?.raw{ - attchmentString = attchmentRaw - } - var icon = (v2msg?.isSelf ?? false) ? "✉️" :"💌" - - return "\(icon)[\(cellType.rawValue)] 『\(v2msg?.text ?? "-")』raw:\(attchmentString)" - } - - var debugDescription: String { - return "[\(cellType.rawValue)] 『\(v2msg?.text ?? "-")』" - } -} diff --git a/crush/Crush/Src/Modules/Chat/Phone/PhoneCallController.swift b/crush/Crush/Src/Modules/Chat/Phone/PhoneCallController.swift deleted file mode 100644 index 3f7b7c2..0000000 --- a/crush/Crush/Src/Modules/Chat/Phone/PhoneCallController.swift +++ /dev/null @@ -1,855 +0,0 @@ -// -// PhoneCallController.swift -// Crush -// -// Created by Leon on 2025/8/19. -// - -import Lottie -import UIKit -import Combine -import DateToolsSwift -import SwiftDate - -enum PhoneCallUIState: Int { - case onCallAIDefault = 0 - case calling = 1 // 拨通了,表示STAR调用成功 - case onCallAISaying = 2 - case onCallAIListening = 3 // 即用户在说话 - case onCallAIThinking = 4 - - var display:String{ - switch self { - case .onCallAIDefault: - return "默认状态,未接通" - case .calling: - return "接通电话" - case .onCallAISaying: - return "AI Saying" - case .onCallAIListening: - return "AI Listening" - case .onCallAIThinking: - return "AI thinking" - } - } -} - -class PhoneCallController: CLBaseViewController, PhoneCallViewModelDelegate { - - var bgIv: UIImageView! - var effectView: UIVisualEffectView! - - // Case 1 - var callingAvatar: CLImageView! - - // Case 2 - var avatarsContainer: UIView! - var avatarStackH: UIStackView! - var avatar1: CLImageView! - var avatar2: CLImageView! - var hearIcon: UIImageView! - var heartLottie: LottieAnimationView! - var heartLevelLabel: UILabel! - - var nameLabel: UILabel! - var heartLevelTag: RelationshipTag! - /// 升降级的NoticeView - var upDownNoticeView : SessionHeartLevelNoticeView! - - // Bottom - - var hangUpButton: EPIconDestructiveButton! - var stateOfCallLabel: CLLabel! - var interruptButton: StyleButton! - var threeDotsAnimationView: LottieAnimationView! - var wavingVoiceView : PhontVoiceWavingView! - var wordsScrollContainer: LTScrollContainer! - var spacerView: UIView! - var wordsOtherSayLabel: LineSpaceLabel! - - lazy var testLabel = UILabel() - lazy var testButton = UIButton() - - // MARK: Flag - var callStartDate: Date = Date() - var testStepIndex: Int = 0 - - // MARK: Data - var aiId : Int? - var viewModel = PhoneCallViewModel() - @Published var viewState: PhoneCallUIState = .onCallAIDefault - private var cancellables = Set() - - var ringTone: SpeechModel? - - // MARK: AI说话超时控制 - /// AI说话超时时间(秒),默认1秒 - var aiSpeakingTimeout: TimeInterval = 1.0 - private var aiSpeakingTimer: Timer? - - // MARK: 用户说话超时控制 - /// 用户停止说话后切换到思考状态的超时时间(秒),默认2秒 - var userSpeakingTimeout: TimeInterval = 1.0 - private var userSpeakingTimer: Timer? - /// 标记用户是否刚刚说过话,只有用户说话后停止说话才会切换到thinking状态 - private var userJustSpoke: Bool = false - - // MARK: - Control - lazy var timestampThisTime = Date().timeStamp - - /// 回调电话状态,duration:(单位毫秒) - var callEventAction:((_ type: CallType, _ duration: Int? ) -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - setupViews() - setupDatas() - setupEvents() - playRingtone() - #if DEBUG - testUI() - #endif - } - - private func setupDatas() { - - guard let user = IMAIViewModel.shared.aiIMInfo else{ - return - } - aiId = user.aiId - viewModel.aiId = aiId - viewModel.delegate = self - - refreshViews() - // Default state - viewState = .onCallAIDefault - -// #warning("test") - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {[weak self] in - self?.startCall() - } - } - - private func refreshViews(){ - guard let user = IMAIViewModel.shared.aiIMInfo else{ - return - } - bgIv.loadImage(user.backgroundImg) - - // 心动等级 - heartLevelLabel.text = "\(user.aiUserHeartbeatRelation?.heartbeatLevelNum ?? 1)" - nameLabel.text = user.nickname - callingAvatar.loadImage(user.headImg) - avatar1.loadImage(user.headImg) - avatar2.loadImage(UserCore.shared.user?.headImage) - - refreshOnlyRelationTag(user.aiUserHeartbeatRelation?.heartbeatVal) - } - - private func refreshOnlyRelationTag(_ val : CGFloat?){ - guard let user = IMAIViewModel.shared.aiIMInfo else{ - return - } - heartLevelTag.bind(level: user.aiUserHeartbeatRelation?.heartbeatLevel, heartBeatVal: val, isShow: user.aiUserHeartbeatRelation?.isShow) - } - - private func startCall(){ - viewModel.loadRtc {[weak self] result in - guard let `self` = self else { - return - } - - if result{ - self.viewModel.opt(type: .START, timestamp: self.timestampThisTime) {[weak self] result in - Hud.hideIndicator() - if result{ - SpeechManager.shared.stopPlayCurrent() - self?.callStartDate = Date() - self?.viewState = .calling - }else{ - self?.tapHangUp() - } - } - }else{ - Hud.hideIndicator() - } - } - } - - - private func testData(){ - bgIv.image = UIImage(named: "egpic")?.cropImageTop(with: 1 / UIScreen.aspectRatio) - - heartLevelLabel.text = "1" - nameLabel.text = "ASAF" - stateOfCallLabel.text = "Click to interrupt" - - let url = UserCore.shared.user?.headImage - avatar1.loadImage(url) - avatar2.loadImage(url) - - let addition = "(Show surprise)" - let words = "Hi, why did you remember to call me? It's not easy \(addition)" - let aStr = words.withAttributes([.font(.t.tbl), .textColor(.text)]) - let ranges = words.matchStrRange(addition) // words.range(of: addition) - let att = [NSAttributedString.Key.font: UIFont.t.tbl, - NSAttributedString.Key.foregroundColor: UIColor.c.ctsn, - ] - for range in ranges { - aStr.addAttributes(att, range: range) - } - wordsOtherSayLabel.attributedText = aStr - - threeDotsAnimationView.play() - } - - private func setupEvents() { - navigationView.tapBackButtonAction = {[weak self] in - self?.tapHangUp() - } - - $viewState.sink {[weak self] state in - #if DEBUG - //Hud.hideToast() - //Hud.toast(str: state.display) - self?.testLabel.text = state.display - #endif - self?.refreshViewsState(state: state) - }.store(in: &cancellables) - - IMAIViewModel.shared.$aiIMInfo.sink {[weak self] info in - self?.refreshViews() - }.store(in: &cancellables) - - IMAIViewModel.shared.$heartbeatVal.sink {[weak self] val in - dlog("Only $heartbeatVal \(String(describing: val)) ") - self?.refreshOnlyRelationTag(val) - }.store(in: &cancellables) - - IMAIViewModel.shared.$heartbeatLevelUpString.sink {[weak self] string in - guard let notice = string, notice.count > 0 else {return} - self?.heartLottie.isHidden = false - self?.heartLottie.play {[weak self] completed in - self?.heartLottie.isHidden = true - } - self?.heartLevelTag.isHidden = true - self?.upDownNoticeView.showUnlocked(string: notice) { - self?.heartLevelTag.isHidden = false - } - }.store(in: &cancellables) - - IMAIViewModel.shared.$heartbeatLevelDownString.sink { [weak self] string in - guard let notice = string, notice.count > 0 else {return} - self?.heartLottie.isHidden = false - self?.heartLottie.play {[weak self] completed in - self?.heartLottie.isHidden = true - } - self?.heartLevelTag.isHidden = true - self?.upDownNoticeView.showLose(string: notice) { - self?.heartLevelTag.isHidden = false - } - }.store(in: &cancellables) - - viewModel.subTitleCallbck = {[weak self] text, userIdString in - if let userId = Int(userIdString), userId == UserCore.shared.user?.userId{ - // 本人在说话 - self?.viewState = .onCallAIListening - // 取消AI说话定时器 - self?.clearAISpeakingTimer() - - // 重置用户说话定时器 - self?.resetUserSpeakingTimer() - - // 标记用户刚刚说过话 - self?.userJustSpoke = true - }else{ - self?.viewState = .onCallAISaying - - // 重置AI说话定时器 - self?.resetAISpeakingTimer() - - // 取消用户说话定时器 - self?.clearUserSpeakingTimer() - - self?.wordsOtherSayLabel.attributedText = self?.formatAttrubuteString(string: text) - if let labelHeight = self?.wordsOtherSayLabel.size.height, let containerHeight = self?.wordsScrollContainer.size.height{ - //dlog("label's height\(labelHeight), stack' s height\(containerHeight)") - if labelHeight < containerHeight{ - self?.spacerView.snp.updateConstraints { make in - make.height.equalTo(containerHeight - labelHeight) - } - }else{ - self?.spacerView.snp.updateConstraints { make in - make.height.equalTo(0) - } - } - } - // 自动滚动到底部 - self?.scrollToBottom() - } - } - - viewModel.leaveTheRoomForcedAction = {[weak self] in - self?.close() - } - - } - - func phoneCallLocalUserSaying(saying: Bool){ - print("💬 syaing: \(saying)") - - if viewState == .onCallAIListening{ - // #warning("to do, check状态对不对,待测试") - wavingVoiceView.play() - } - } - - // MARK: - Action - @objc private func tapHangUp() { - -// #warning("test") -// viewModel.leaveRoom() -// close(dismissFirst: true) -// return - -// #warning("test") -// //callEventAction?(.CALL_CANCEL, nil) -// callEventAction?(.CALL_END, 6500) -// close(dismissFirst: true) -// return - - // 清理AI说话定时器 - clearAISpeakingTimer() - - guard viewState.rawValue >= PhoneCallUIState.calling.rawValue else{ - viewModel.leaveRoom() - callEventAction?(.CALL_CANCEL, nil) - close(dismissFirst: true) - return - } - - let now = Date() - let miniseconds = now.timeStamp - self.callStartDate.timeStamp - - Hud.showIndicator() - viewModel.opt(type: .STOP, timestamp: timestampThisTime, duration: miniseconds) {[weak self] result in - Hud.hideIndicator() - guard let self = self else{return} - if result{ - self.viewModel.leaveRoom() - - self.callEventAction?(.CALL_END, miniseconds) - // 是否退出页面 - self.close(dismissFirst: true) - } - } - } - - @objc private func tapInterruptButton(){ - Hud.showIndicator() - viewModel.opt(type: .INTERRUPT, timestamp: timestampThisTime) { _ in - Hud.hideIndicator() - } - } - - // MARK: - Functions - - private func playRingtone() { - guard let path = Bundle.main.path(forResource: "call_ringtone", ofType: "mp3") else { return } - let model = SpeechManager.shared.modelWithFilePath(path) - ringTone = model - - model.stateChangedBlock = { [weak self] updatedModel in - guard let self = self else { return } - self.checkState(model: updatedModel) - } - - SpeechManager.shared.startPlay(with: model) - } - - private func checkState(model: SpeechModel) { - guard model.loadState == .complete else { - // SpeechManager.shared.stopPlay(with: model) // ❌ - return - } - - switch model.playState { - case .default: - if model.canAutoPlay { - SpeechManager.shared.startPlay(with: model) - } - case .complete, .failed: - SpeechManager.shared.stopPlay(with: model) - case .playing: - break - } - } - - // MARK: - Helper - - private func scrollToBottom() { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - let bottomOffset = CGPoint(x: 0, y: max(0, self.wordsScrollContainer.scrollView.contentSize.height - self.wordsScrollContainer.scrollView.bounds.height)) - self.wordsScrollContainer.scrollView.setContentOffset(bottomOffset, animated: true) - } - } - - /// 重置AI说话定时器 - private func resetAISpeakingTimer() { - // 取消之前的定时器 - aiSpeakingTimer?.invalidate() - - // 创建新的定时器 - aiSpeakingTimer = Timer.scheduledTimer(withTimeInterval: aiSpeakingTimeout, repeats: false) { [weak self] _ in - // 超时后切换到AI不说话状态 - self?.viewState = .onCallAIListening - - self?.wavingVoiceView.stop() - } - } - - /// 清理AI说话定时器 - private func clearAISpeakingTimer() { - aiSpeakingTimer?.invalidate() - aiSpeakingTimer = nil - } - - /// 重置用户说话定时器 - private func resetUserSpeakingTimer() { - // 取消之前的定时器 - userSpeakingTimer?.invalidate() - - // 说明还在说话中... - if wavingVoiceView.isAnimationPlaying == false{ - wavingVoiceView.play() - } - - // 创建新的定时器 - userSpeakingTimer = Timer.scheduledTimer(withTimeInterval: userSpeakingTimeout, repeats: false) { [weak self] _ in - // 只有用户刚刚说过话,停止说话才切换到思考状态 - if self?.userJustSpoke == true { - self?.viewState = .onCallAIThinking - self?.userJustSpoke = false // 重置标志 - } - - } - } - - /// 清理用户说话定时器 - private func clearUserSpeakingTimer() { - userSpeakingTimer?.invalidate() - userSpeakingTimer = nil - } - - private func showThreeDotAnimation(_ show: Bool){ - if show{ - threeDotsAnimationView.isHidden = false - threeDotsAnimationView.play() - }else{ - threeDotsAnimationView.isHidden = true - threeDotsAnimationView.stop() - } - } - - private func showWaveWavingAnimation(_ show: Bool){ - if show{ - wavingVoiceView.isHidden = false - wavingVoiceView.play() - }else{ - wavingVoiceView.isHidden = true - wavingVoiceView.stop() - } - } - - private func refreshViewsState(state: PhoneCallUIState){ - switch state { - case .onCallAIDefault: - showThreeDotAnimation(true) - showWaveWavingAnimation(false) - stateOfCallLabel.isHidden = false - callingAvatar.isHidden = false - avatarsContainer.isHidden = true - interruptButton.isHidden = true - threeDotsAnimationView.play() - stateOfCallLabel.text = "Waiting to be connected"//"..." // 初始的加载过程中 - case .calling: - showThreeDotAnimation(true) - showWaveWavingAnimation(false) - stateOfCallLabel.isHidden = false - callingAvatar.isHidden = true - avatarsContainer.isHidden = false - interruptButton.isHidden = true - threeDotsAnimationView.play() - stateOfCallLabel.text = " "//"Waiting to be connected" - - case .onCallAISaying: - showThreeDotAnimation(false) - showWaveWavingAnimation(false) - stateOfCallLabel.isHidden = true - callingAvatar.isHidden = true - avatarsContainer.isHidden = false - interruptButton.isHidden = false - threeDotsAnimationView.stop() - break - case .onCallAIListening: - showThreeDotAnimation(false) - showWaveWavingAnimation(true) - stateOfCallLabel.isHidden = false - callingAvatar.isHidden = true - avatarsContainer.isHidden = false - interruptButton.isHidden = true - stateOfCallLabel.text = "Listening..." - break - case .onCallAIThinking: - showThreeDotAnimation(true) - showWaveWavingAnimation(false) - stateOfCallLabel.isHidden = false - callingAvatar.isHidden = true - avatarsContainer.isHidden = false - interruptButton.isHidden = true - stateOfCallLabel.text = "Thinking..." - } - } - - private func formatAttrubuteString(string: String) -> NSMutableAttributedString{ - let content = string - let aStr = content.withAttributes([.font(.t.tbm), .textColor(.text)]) - let ranges = String.findBracketRanges(in: content) - let att = [NSAttributedString.Key.font: UIFont.t.tbm, - NSAttributedString.Key.foregroundColor: UIColor.c.ctsn, - ] - for range in ranges { - aStr.addAttributes(att, range: range) - } - return aStr - } - - // MARK: - UI - - private func setupViews() { - navigationView.setupBackButtonCloseIcon() - navigationView.bgView.alpha = 0 - - bgIv = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.top.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - v.alpha = 0.9 - view.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - callingAvatar = { - let v = CLImageView() - v.cornerRadius = 64 - v.backgroundColor = .c.csbn - view.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 128, height: 128)) - make.centerX.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom).offset(48) - } - return v - }() - - avatarsContainer = { - let v = UIView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom).offset(48) - make.height.equalTo(128) - } - v.isHidden = true - return v - }() - - avatarStackH = { - let v = UIStackView() - v.spacing = 16 - avatarsContainer.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - avatar1 = { - let v = CLImageView() - v.cornerRadius = 64 - v.backgroundColor = .c.csbn - avatarStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 128, height: 128)) - } - return v - }() - - avatar2 = { - let v = CLImageView() - v.cornerRadius = 64 - v.backgroundColor = .c.csbn - avatarStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 128, height: 128)) - } - return v - }() - - hearIcon = { - let v = UIImageView() - v.image = UIImage(named: "chat_level_heart") - avatarsContainer.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 80, height: 80)) - make.center.equalToSuperview() - } - return v - }() - - heartLottie = { - let animation = LottieAnimation.named("heartbeat_pink") - let animationView = LottieAnimationView(animation: animation) - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .playOnce - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - animationView.size = CGSize(width: 44, height: 44) - //avatarsContainer.addSubview(animationView) - avatarsContainer.insertSubview(animationView, belowSubview: hearIcon) - animationView.snp.makeConstraints { make in - make.center.equalTo(hearIcon) - make.size.equalTo(CGSize(width: 240, height: 210)) - } - animationView.isHidden = true - return animationView - }() - - heartLevelLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tnml - avatarsContainer.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalTo(hearIcon) - } - return v - }() - - nameLabel = { - let v = UILabel() - v.font = .t.ttl - v.textColor = .text - v.textAlignment = .center - view.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(avatarStackH.snp.bottom).offset(16) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - - heartLevelTag = { - let v = RelationshipTag(size: .large) - v.effectView.alpha = 0 - view.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(nameLabel.snp.bottom).offset(16) - } - return v - }() - - upDownNoticeView = { - let v = SessionHeartLevelNoticeView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(nameLabel.snp.bottom).offset(16) - } - return v - }() - - setupBottomView() - - view.bringSubviewToFront(navigationView) - } - - private func setupBottomView() { - hangUpButton = { - let v = EPIconDestructiveButton(radius: .round, iconSize: .large, iconCode: .iconCallHangup) - v.addTarget(self, action: #selector(tapHangUp), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSizeMake(48, 48)) - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-24 - UIWindow.safeAreaBottom - 16) - } - - return v - }() - - stateOfCallLabel = { - let v = CLLabel() - v.font = .t.tlm - v.textColor = .desc - v.textAlignment = .center - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalTo(hangUpButton.snp.top).offset(-24) - } - return v - }() - - threeDotsAnimationView = { - let animation = LottieAnimation.named("three_dots_msg_loading") - let animationView = LottieAnimationView(animation: animation) - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - animationView.size = CGSize(width: 70, height: 40) - view.addSubview(animationView) - animationView.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 70, height: 40)) - make.centerX.equalToSuperview() - //make.bottom.equalTo(stateOfCallLabel.snp.top).offset(-16) // -24 - make.centerY.equalTo(stateOfCallLabel.snp.top).offset(-24) - } - return animationView - }() - - wavingVoiceView = { - let v = PhontVoiceWavingView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 156, height: 24)) - make.centerX.equalToSuperview() - //make.bottom.equalTo(stateOfCallLabel.snp.top).offset(-24) - make.centerY.equalTo(threeDotsAnimationView) - } - return v - }() - - interruptButton = { - let v = StyleButton(type: .custom) - v.tertiary(size: .small) - v.setTitle("Interrupt", for: .normal) - v.addTarget(self, action: #selector(tapInterruptButton), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalTo(hangUpButton.snp.top).offset(-24) - } - v.isHidden = true - return v - }() - - wordsScrollContainer = { - let v = LTScrollContainer() - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(heartLevelTag.snp.bottom).offset(16) - //make.bottom.equalTo(stateOfCallLabel.snp.top).offset(-88) - make.bottom.equalTo(hangUpButton.snp.top).offset(-108) - } - return v - }() - // 添加一个占位视图,让字幕始终显示在底部 - let spacerView = UIView() - wordsScrollContainer.stack.addArrangedSubview(spacerView) - spacerView.snp.makeConstraints { make in - //make.height.greaterThanOrEqualTo(0) - make.height.equalTo(0) - } - self.spacerView = spacerView - - wordsOtherSayLabel = { - let v = LineSpaceLabel() - v.textColor = .white - wordsScrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - - - - // 设置 stack 的对齐方式,让内容靠底部显示 - wordsScrollContainer.stack.alignment = .fill - wordsScrollContainer.stack.distribution = .fill - } - - deinit { - clearAISpeakingTimer() - clearUserSpeakingTimer() - } - - // MARK: - Test - - private func testUI(){ - view.addSubview(testLabel) - testLabel.textColor = .white - testLabel.snp.makeConstraints { make in - make.top.equalToSuperview().offset(UIWindow.statusBarHeight + 8) - make.centerX.equalToSuperview() - } - - view.addSubview(testButton) - testButton.setTitle("Switch view State", for: .normal) - testButton.backgroundColor = .random - testButton.snp.makeConstraints { make in - make.top.equalTo(testLabel.snp.bottom).offset(12) - make.centerX.equalToSuperview() - } - testButton.addTarget(self, action: #selector(testCall), for: .touchUpInside) - - let close = UIButton() - view.addSubview(close) - close.backgroundColor = .random - close.setTitle("Close", for: .normal) - close.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(testButton.snp.bottom).offset(8) - } - close.addTap {[weak self] btn in - self?.close() - } - } - - @objc private func testCall(){ -// self.testStepIndex += 1 -// let nextState = PhoneCallUIState(rawValue: self.testStepIndex) ?? .onCallAIDefault -// self.viewState = nextState - - viewState = .calling - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - IMAIViewModel.shared.heartbeatLevelUpString = "xxxxxxxxx" - } - - } -} diff --git a/crush/Crush/Src/Modules/Chat/Phone/View/PhoneVoiceWavingView.swift b/crush/Crush/Src/Modules/Chat/Phone/View/PhoneVoiceWavingView.swift deleted file mode 100644 index a922dd2..0000000 --- a/crush/Crush/Src/Modules/Chat/Phone/View/PhoneVoiceWavingView.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// PhoneVoiceWavingView.swift -// Crush -// -// Created by Leon on 2025/9/26. -// - -import UIKit -import Lottie -class PhontVoiceWavingView: UIView{ - var waveWavingAnimationView: LottieAnimationView! - var waveSilentView: UIImageView! - - var isAnimationPlaying : Bool{ - return waveWavingAnimationView.isAnimationPlaying - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - - waveWavingAnimationView = { - let animation = LottieAnimation.named("voice_im_recording") - let animationView = LottieAnimationView(animation: animation) - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - animationView.size = CGSize(width: 156, height: 24) - addSubview(animationView) - animationView.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 156, height: 24)) - make.center.equalToSuperview() - } - animationView.isHidden = true - return animationView - }() - - waveSilentView = { - let v = UIImageView() - v.image = UIImage(named: "ai_phone_call_silent") - addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 156, height: 8)) - make.center.equalTo(waveWavingAnimationView) - } - v.isHidden = true - return v - }() - } - - func play(){ - waveWavingAnimationView.isHidden = false - waveSilentView.isHidden = true - waveWavingAnimationView.play() - } - - func stop(){ - waveWavingAnimationView.isHidden = true - waveSilentView.isHidden = false - waveWavingAnimationView.stop() - } -} diff --git a/crush/Crush/Src/Modules/Chat/Phone/ViewModel/PhoneCallViewModel.swift b/crush/Crush/Src/Modules/Chat/Phone/ViewModel/PhoneCallViewModel.swift deleted file mode 100644 index df9b847..0000000 --- a/crush/Crush/Src/Modules/Chat/Phone/ViewModel/PhoneCallViewModel.swift +++ /dev/null @@ -1,331 +0,0 @@ -// -// PhoneCallViewModel.swift -// Crush -// -// Created by Leon on 2025/8/27. -// - -import Foundation -import BytePlusRTC -import AVFoundation - -enum PhoneCallOptType: String { - case START - case INTERRUPT - case STOP -} - -protocol PhoneCallViewModelDelegate: AnyObject{ - func phoneCallLocalUserSaying(saying: Bool) -} - -class PhoneCallViewModel: NSObject { - weak var delegate: PhoneCallViewModelDelegate? - - /// text, userId(String type) - var subTitleCallbck: ((String, String)-> Void)? - - private(set) var token: String? - - var aiId : Int?{ - didSet{ - roomId = "\(UserCore.shared.user?.userId ?? 0)-\(aiId ?? 0)" - } - } - var roomId : String? - - private(set) var rtcVideo: ByteRTCVideo? - private(set) var rtcRoom: ByteRTCRoom? - - // 字幕相关 - var aiCompleteMessage = [String]() - /// 强制离开房间 - var leaveTheRoomForcedAction : (()->Void)? - - func loadRtc(completion: ((Bool)-> Void)?) { - guard let rtcRoomId = roomId else {return} - AICowProvider.request(.voiceAICallRtcToken(roomId: rtcRoomId), modelType: PhoneCallRtcGetResponse.self) { [weak self] result in - switch result { - case let .success(model): - self?.token = model?.token - self?.buildRTCEngine() - self?.joinRoom() - completion?(true) - case .failure: - completion?(false) - } - } - } - - func opt(type: PhoneCallOptType, timestamp:Int, duration: Int = 0, completion:((Bool)->Void)?){ - guard let rtcRoomId = roomId, let aiUid = aiId else{return} - - let taskId = "\(rtcRoomId)-\(timestamp)" - - var params = [String:Any]() - params.updateValue(aiUid, forKey: "aiId") - params.updateValue(rtcRoomId, forKey: "roomId") - params.updateValue(taskId, forKey: "taskId") - params.updateValue(type.rawValue, forKey: "optType") - - if duration > 0{ - params.updateValue(duration, forKey: "duration") - } - - AICowProvider.request(.voiceCallOperate(params: params), modelType: EmptyModel.self) {[weak self] result in - switch result { - case .success: - completion?(true) - case .failure(let failure): - switch failure { - case let .serviceError(code, _): - if code == .insufficentCoin{ - // 离开rtc - self?.leaveRoom() - // 离开打电话 - self?.leaveTheRoomForcedAction?() - } - default: - break - } - completion?(false) - break - } - } - } - deinit { - destoryBytePlus() - } - -} - -extension PhoneCallViewModel{ - public func leaveRoom(){ - self.rtcVideo?.logout() - self.rtcRoom?.leaveRoom() - } - - private func destoryBytePlus() { - self.rtcRoom?.destroy() - ByteRTCVideo.destroyRTCVideo(); - self.rtcVideo = nil - self.rtcRoom = nil - } - - private func buildRTCEngine() { - /// Create engine objects. - self.rtcVideo = ByteRTCVideo.createRTCVideo("689ade491323ae01797818e0", delegate: self, parameters: [:]) - - /// Start pushing multiple video streams and set the video parameters when pushing multiple streams, - /// Including resolution, frame rate, bit rate, zoom mode, fallback strategy when the network is poor, etc. - - let encoderCfg = ByteRTCVideoEncoderConfig.init() - encoderCfg.width = 360 - encoderCfg.height = 640 - encoderCfg.frameRate = 15 - encoderCfg.maxBitrate = 800 - self.rtcVideo?.setMaxVideoEncoderConfig(encoderCfg) - - /// Enable internal video capture immediately. The default is off. - // self.rtcVideo?.startVideoCapture() - - // 开启音量上报 - let audioPropertiesConfig = ByteRTCAudioPropertiesConfig() - audioPropertiesConfig.interval = 200 - self.rtcVideo?.enableAudioPropertiesReport(audioPropertiesConfig) - - /// Enables internal audio capture. The default is off. - self.rtcVideo?.startAudioCapture() - } - - private func joinRoom() { - guard let rtcRoomId = roomId, let roomToken = token else{ - return - } - // joinroom - self.rtcRoom = self.rtcVideo?.createRTCRoom(rtcRoomId) - self.rtcRoom?.delegate = self - let userInfo = ByteRTCUserInfo.init() - userInfo.userId = "\(UserCore.shared.user?.userId ?? 0)" - let roomCfg = ByteRTCRoomConfig.init() - roomCfg.isAutoPublish = true - roomCfg.isAutoSubscribeAudio = true - //roomCfg.isAutoSubscribeVideo = true - self.rtcRoom?.joinRoom(roomToken, userInfo: userInfo, roomConfig: roomCfg) - self.rtcRoom?.publishStream(.audio) // self.rtcRoom?.unpublishStream(.audio) - } - - - -} - -extension PhoneCallViewModel: ByteRTCRoomDelegate{ - func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserJoined userInfo: ByteRTCUserInfo, elapsed: Int) { - dlog("☎️user joined: \(userInfo)") - } - - func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserLeave uid: String, reason: ByteRTCUserOfflineReason) { - dlog("☎️user leaved: uid:\(uid), reason: \(reason)") - } - - func rtcEngine(_ engine: ByteRTCVideo, onError errorCode: ByteRTCErrorCode) { - NSLog("☎️onError: %zd", errorCode.rawValue) - } - - func rtcEngine(_ engine: ByteRTCVideo, onWarning Code: ByteRTCWarningCode) { - NSLog("☎️onWarning: %zd", Code.rawValue) - } -} - -extension PhoneCallViewModel: ByteRTCVideoDelegate{ -// func rtcEngine(_ engine: ByteRTCVideo, onActiveSpeaker roomId: String, uid: String) { // 此方法不会回调 -// print("roomId: \(roomId), uid is speaking:\(uid)") -// } - - func rtcRoom(_ rtcRoom: ByteRTCRoom, onRoomBinaryMessageReceived uid: String, message: Data) { - let subtitles = SubtitleParser.parse(from: message) - for sub in subtitles { - DispatchQueue.main.async {[weak self] in - if "\(UserCore.shared.user?.userId ?? 0)" == sub.userId{ - print("用户字幕: \(sub.text), 用户: \(sub.userId), 时间戳: \(sub.timestamp ?? 0)") - // 表示是我自己在说话,清空所有暂存的ai说的话。 - self?.aiCompleteMessage.removeAll() - self?.subTitleCallbck?(sub.text, sub.userId) - }else{ - print("AI字幕: \(sub.text), AI: \(sub.userId), 时间戳: \(sub.timestamp ?? 0)") - // AI在说话 - self?.handleAISubtitle(sub) - } - } - } - - /* - { - "data": [ - { - "definite": true, - "language": "zh", - "mode": 0, - "paragraph": true, - "roundId": 0, - "sequence": 15, - "text": "你好啊,我是小爱同学,很高兴认识你。", - "timestamp": 1756273528308, - "userId": "439217670979585" - } - ], - "type": "subtitle" - } - */ - -// if let jsonString = SubtitleParser.parseRawJSON(from: message) { -// print("👉 解包后JSON: \(jsonString)") -// } - } - - /// 处理AI字幕的拼接逻辑 - private func handleAISubtitle(_ sub: SubtitleMsgData) { - // 判断一句话是否说完 - let isComplete = sub.paragraph || sub.definite - - if isComplete { - // 情况1: paragraph为true 或 情况2: paragraph为false但definite为true - // 将完整的text添加到aiCompleteMessage中 - aiCompleteMessage.append(sub.text) - // 显示拼接后的完整字幕 - let completeSubtitle = aiCompleteMessage.joined(separator: "\n") - subTitleCallbck?(completeSubtitle, sub.userId) - } else { - // paragraph和definite都为false,表示这句话还没说完 - // 显示暂存字幕 + 当前更新的text - let currentSubtitle = aiCompleteMessage.joined(separator: "\n") + (aiCompleteMessage.isEmpty ? "" : "\n") + sub.text - subTitleCallbck?(currentSubtitle, sub.userId) - } - } - - func rtcRoom(_ rtcRoom: ByteRTCRoom, onRoomStateChanged roomId: String, withUid uid: String, state: Int, extraInfo: String) { - NSLog("☎️onRoomStateChanged uid:\(uid), extraInfo:\(extraInfo)") - } -} - -extension PhoneCallViewModel{ - func rtcEngine(_ engine: ByteRTCVideo, onLocalAudioPropertiesReport audioPropertiesInfos: [ByteRTCLocalAudioPropertiesInfo]) { - var imSaying = false - for info in audioPropertiesInfos { - if info.audioPropertiesInfo.linearVolume > 25{ // 表示我在说话 - imSaying = true - break - } - } - - delegate?.phoneCallLocalUserSaying(saying: imSaying) - } -} - -// MARK: - Helper字幕解析 -// 对应字幕数据结构 -struct SubtitleMsgData: Codable { - let definite: Bool - let language: String - let paragraph: Bool - let sequence: Int - let text: String - let userId: String - - // 新增的可选字段 - let mode: Int? - let roundId: Int? - let timestamp: Int? -} - -// 根结构 -struct SubtitleRoot: Codable { - let data: [SubtitleMsgData] - let type: String -} - -enum SubtitleParser { - private static let kSubtitleHeaderSize = 8 - private static let magicNumber: UInt32 = 0x73756276 - - private static func unpackMessage(_ message: Data) -> String? { - guard message.count >= kSubtitleHeaderSize else { return nil } - - // 读取 magic number - let magic = message.prefix(4).reduce(0) { ($0 << 8) | UInt32($1) } - guard magic == magicNumber else { return nil } - - // 长度 - let length = message.dropFirst(4).prefix(4).reduce(0) { ($0 << 8) | UInt32($1) } - guard message.count - kSubtitleHeaderSize == length else { return nil } - - // JSON 字符串 - let jsonData = message.dropFirst(kSubtitleHeaderSize) - return String(data: jsonData, encoding: .utf8) - } - - /// 最终解析方法,返回 `[SubtitleMsgData]` - static func parse(from message: Data) -> [SubtitleMsgData] { - guard let jsonString = unpackMessage(message), - let jsonData = jsonString.data(using: .utf8) else { - return [] - } - - do { - let root = try JSONDecoder().decode(SubtitleRoot.self, from: jsonData) - return root.data - } catch { - print("Subtitle JSON parse error: \(error)") - return [] - } - } - - /// 调试用:返回解包后的 JSON - static func parseRawJSON(from message: Data) -> String? { - return unpackMessage(message) - } -} -// MARK: - Other -class PhoneCallRtcGetResponse: Codable { - var token: String? -} diff --git a/crush/Crush/Src/Modules/Chat/Session/Input/GiftGridSendView.swift b/crush/Crush/Src/Modules/Chat/Session/Input/GiftGridSendView.swift deleted file mode 100644 index 45773a3..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/Input/GiftGridSendView.swift +++ /dev/null @@ -1,355 +0,0 @@ -// -// GiftGridSendView.swift -// Crush -// -// Created by Leon on 2025/8/22. -// - -import UIKit -import Combine - -protocol GiftGridSendViewDelegate : AnyObject{ - func giftSend(giftId: Int, gift: GiftDictModel, num: Int) - func giftGoHeartbeatPage() - func giftGoVIPCenter() - /// 外部去选择数量 - func giftNumPick(num: Int) -} - -class GiftGridSendView: UIView { - weak var delegate : GiftGridSendViewDelegate? - - var bgView: UIView! - var cv: UICollectionView! - var layout = UICollectionViewFlowLayout() - - var bottomView: UIView! - var coinLabel: CLIconLabel! - - var sendContainer: UIView! - var operateButton: StyleButton! - - var countContainer: UIView! - var countLabel: CLLabel! - var selectCountButton: UIButton! - var countImg : UIImageView! - // -- Data - - /// @Requered - @Published var targetHeartbeatValue: CGFloat? - - var gifts: [GiftDictModel] = [GiftDictModel]() - @Published var selectGiftId: Int? - @Published var num: Int = 1 - - var selectGift : GiftDictModel? - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csbn - - bgView = { - let v = UIView() - v.backgroundColor = backgroundColor - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview().offset(UIWindow.safeAreaBottom + 16) - } - return v - }() - - bottomView = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(80) - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview()//.offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - - cv = { - let itemsInRow = 3.0 - let width = floor((UIScreen.width - 24.0 * 2) * (1 / itemsInRow)) - let height = 88.0 + 52.0// 140.0 //width + 24 - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 0 - layout.sectionInset = UIEdgeInsets(top: 16, left: 24, bottom:16, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(IMGiftGridCell.self, forCellWithReuseIdentifier: "IMGiftGridCell") - addSubview(cv) - cv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalTo(bottomView.snp.top) - make.height.equalTo(200) - } - return cv - }() - - coinLabel = { - let label = CLIconLabel() - label.iconImageView.image = UIImage(named: "icon_16_diamond") - label.contentLabel.font = .t.tlm - bottomView.addSubview(label) - label.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(24) - } - return label - }() - - sendContainer = { - let v = UIView() - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-24) - make.height.equalTo(48) - } - return v - }() - - operateButton = { - let v = StyleButton() - v.addTarget(self, action: #selector(tapOperateButton), for: .touchUpInside) - v.primary(size: .large) - sendContainer.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - do { - countContainer = { - let v = UIView() - sendContainer.addSubview(v) - v.layer.borderWidth = 1 - v.layer.borderColor = UIColor.c.con.cgColor - v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] - v.cornerRadius = 24 - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.bottom.top.equalToSuperview() - make.trailing.equalTo(operateButton.snp.leading).offset(24) - } - return v - }() - - let downArrow = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .arrowDownFill, size: CGSize(width: 16, height: 16), color: .c.ctsn) - countContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-24-12) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - return v - }() - countImg = downArrow - - countLabel = { - let v = CLLabel() - v.font = .t.tll - countContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalTo(downArrow.snp.leading).offset(-12) - make.leading.equalToSuperview().offset(32) - } - return v - }() - - selectCountButton = { - let v = UIButton() - v.addTarget(self, action: #selector(tapSelectCountButton), for: .touchUpInside) - countContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - } - sendContainer.bringSubviewToFront(operateButton) - - operateButton.setTitle("Gift", for: .normal) - } - - private func setupData() { - //coinLabel.contentLabel.text = "123" - countLabel.text = "1" - - gifts = AppDictManager.shared.gifts ?? [] - if let gift = gifts.first, let firstGiftId = gift.id{ - selectGiftId = firstGiftId - selectGift = gift - cv.reloadData() - } - - } - - private func setupEvent() { - - $selectGiftId.sink {[weak self] giftId in - self?.operateButton.isEnabled = (giftId ?? 0) > 0 - self?.cv.reloadData() - - if let select = self?.selectGift{ - // 检查选择的礼物是否需要更高等级 - self?.updateOperateButtonTitle(for: select) - } - }.store(in: &cancellables) - - $targetHeartbeatValue.sink {[weak self] val in - self?.selectGiftId = self?.selectGiftId - }.store(in: &cancellables) - - IMAIViewModel.shared.$aiIMInfo.sink {[weak self] result in - if let val = result?.aiUserHeartbeatRelation?.heartbeatVal{ - self?.targetHeartbeatValue = val - } - }.store(in: &cancellables) - - $num.sink {[weak self] number in - self?.countLabel.text = "\(number)" - }.store(in: &cancellables) - - WalletCore.shared.$balance.sink {[weak self] balance in - if let priceLabel = self?.coinLabel { - priceLabel.contentLabel.text = balance.displayBalance() - } - }.store(in: &cancellables) - - NotificationCenter.default.addObserver(self, selector: #selector(notifyVIPChanged(_:)), name: AppNotificationName.vipStateChange.notificationName, object: nil) - } - - @objc private func notifyVIPChanged(_ noti: Notification){ - self.selectGiftId = selectGiftId - } - - // MARK: Public - - func reset(){ - num = 1 - resetSelectGift() - } - - // MARK: - Helper - private func resetSelectGift(){ - if let gift = gifts.first, let firstGiftId = gift.id{ - selectGiftId = firstGiftId - selectGift = gift - cv.reloadData() - } - } - - // MARK: Action - - @objc private func tapOperateButton() { - guard let id = selectGiftId else{ - return - } - - // 获取当前选择的礼物 - guard let gift = gifts.first(where: { $0.id == id }) else { - return - } - - // 检查礼物的startVal是否大于目标用户的心动值 - if let startVal = gift.startVal, - let targetValue = targetHeartbeatValue, - CGFloat(startVal) > targetValue { - // 如果礼物的startVal大于目标用户的心动值,跳转到心动页面 - delegate?.giftGoHeartbeatPage() - } else if gift.isMemberGift.boolValue && UserCore.shared.user?.isMember.boolValue == false{ - delegate?.giftGoVIPCenter() - } - else { - // 否则正常发送礼物 - delegate?.giftSend(giftId: id,gift: gift, num: num) - } - } - - @objc private func tapSelectCountButton() { - delegate?.giftNumPick(num: num) - } -} - -extension GiftGridSendView: UICollectionViewDelegate, UICollectionViewDataSource{ - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return gifts.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "IMGiftGridCell", for: indexPath) as! IMGiftGridCell - let data = gifts[indexPath.item] - cell.config(data) - cell.setupSelected(selectedId: selectGiftId) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let data = gifts[indexPath.item] - selectGift = data - selectGiftId = data.id - } - - // MARK: Private Helper - - private func updateOperateButtonTitle(for gift: GiftDictModel) { - // 检查礼物的startVal是否大于目标用户的心动值 - if let startVal = gift.startVal, - let targetValue = targetHeartbeatValue, - CGFloat(startVal) > targetValue { - countContainer.isHidden = true - operateButton.primary(size: .large) - // 如果礼物的startVal大于目标用户的心动值,显示解锁提示 - var buttonTitle = "Unlock" - if let heartbeatLevel = gift.heartbeatLevel { - let message = "\(heartbeatLevel.localizedText) unlock" - buttonTitle = message - } - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: buttonTitle, textFont: .t.tll,textColor: .white) - operateButton.setAttributedTitle(aStr, for: .normal) - } else if gift.isMemberGift.boolValue && UserCore.shared.user?.isMember.boolValue == false{ - // VIP unlock - countContainer.isHidden = true - // ... - operateButton.vip(size: .large) - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: "Member unlock", icon:.iconVip, textFont: .t.tll) - operateButton.setAttributedTitle(aStr, for: .normal) - } - else { - countContainer.isHidden = false - operateButton.primary(size: .large) - - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: "Gift", textFont: .t.tll,textColor: .white) - operateButton.setAttributedTitle(aStr, for: .normal) - } - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/Input/IMGiftGridCell.swift b/crush/Crush/Src/Modules/Chat/Session/Input/IMGiftGridCell.swift deleted file mode 100644 index 5f82c20..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/Input/IMGiftGridCell.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// IMGiftGridCell.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit - -class IMGiftGridCell: UICollectionViewCell { - var block: UIView! - var icon: UIImageView! - var itemLabel: CLLabel! - var coinLabel: CLIconLabel! - - var data: GiftDictModel? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - contentView.addSubview(v) - v.cornerRadius = 16 - v.layer.borderColor = UIColor.c.cpn.cgColor - v.layer.borderWidth = 0 - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 88, height: 88)) - make.centerX.equalToSuperview() - make.height.equalTo(v.snp.width) - make.top.equalToSuperview() - } - return v - }() - - icon = { - let v = UIImageView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 64, height: 64)) - } - return v - }() - - itemLabel = { - let v = CLLabel() - v.textAlignment = .center - v.font = .t.tls - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(block.snp.bottom).offset(4) - } - return v - }() - - coinLabel = { - let v = CLIconLabel() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(itemLabel.snp.bottom).offset(4) - } - return v - }() - } - - private func setupData() { - coinLabel.iconImageView.image = UIImage(named: "icon_16_diamond") - coinLabel.contentLabel.text = "10" - itemLabel.text = "Friends" - } - - private func setupEvent() { - } - - func config(_ data: GiftDictModel) { - self.data = data - - itemLabel.text = data.name - icon.loadImage(data.icon, bgColor: UIColor.clear) - - let price = data.price ?? 0 - let coin = Double(price) * 0.01 - coinLabel.contentLabel.text = String.thousandString(float: coin, maxDigits: 2) - } - - func setupSelected(selectedId: Int?) { - guard let id = selectedId, let dataId = data?.id else { - block.layer.borderWidth = 0 - return - } - block.layer.borderWidth = id == dataId ? 2 : 0 - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/Input/IMMoreItemView.swift b/crush/Crush/Src/Modules/Chat/Session/Input/IMMoreItemView.swift deleted file mode 100644 index fd474bb..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/Input/IMMoreItemView.swift +++ /dev/null @@ -1,223 +0,0 @@ -// -// IMMoreItemView.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit -import Combine - -enum IMMoreMenuType: Int { - case picture = 0 - case phone = 1 - case share = 2 -} - -protocol IMMoreItemViewDelegate: AnyObject { - func imMoreItemView(view: IMMoreItemView, tapType: IMMoreMenuType) -} - -class IMMoreItemView: UIView { - weak var delegate: IMMoreItemViewDelegate? - - var bgView: UIView! - var cv: UICollectionView! - var layout = UICollectionViewFlowLayout() - - override var isHidden: Bool{ - didSet{ - cv.reloadData() - } - } - - static let heightOfMoreItemView = 206.0 - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csbn - - bgView = { - let v = UIView() - v.backgroundColor = backgroundColor - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview().offset(UIWindow.safeAreaBottom + 16) - } - return v - }() - - cv = { - let itemsInRow = 4.0 - let width = floor((UIScreen.width - 24.0 * 2 - 16.0 * (itemsInRow - 1)) * (1 / itemsInRow)) - let height = width + 24 - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 16, left: 24, bottom: UIWindow.safeAreaBottom + 16, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(IMMoreItemGridCell.self, forCellWithReuseIdentifier: "IMMoreItemGridCell") - addSubview(cv) - cv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview() - make.height.equalTo(IMMoreItemView.heightOfMoreItemView) // 240 - } - return cv - }() - } - - private func setupData() { - } - - private func setupEvent() { - IMAIViewModel.shared.$aiIMInfo.sink {[weak self] user in - self?.cv.reloadData() - }.store(in: &cancellables) - } -} - -extension IMMoreItemView: UICollectionViewDelegate, UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - 3 - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "IMMoreItemGridCell", for: indexPath) as! IMMoreItemGridCell - if let type = IMMoreMenuType(rawValue: indexPath.item) { - cell.config(type: type) - } - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let type = IMMoreMenuType(rawValue: indexPath.item) else { - return - } - - let level = IMAIViewModel.shared.aiIMInfo?.aiUserHeartbeatRelation?.heartbeatLevel - - switch type { - case .picture: - if let theLevel = level, theLevel.isGreaterOrEqual(to: .level2){ - delegate?.imMoreItemView(view: self, tapType: type) - }else{ - AppRouter.goAIHeartBeatLevelPage(aiId: IMAIViewModel.shared.aiId) - } - case .phone: - if let theLevel = level, theLevel.isGreaterOrEqual(to: .level4){ - delegate?.imMoreItemView(view: self, tapType: type) - }else{ - AppRouter.goAIHeartBeatLevelPage(aiId: IMAIViewModel.shared.aiId) - } - case .share: - delegate?.imMoreItemView(view: self, tapType: type) - } - } -} - -class IMMoreItemGridCell: UICollectionViewCell { - var block: UIView! - var icon: UIImageView! - var itemLabel: CLLabel! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.backgroundColor = .c.cseln - v.cornerRadius = 16 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width) - make.top.equalToSuperview() - } - return v - }() - - icon = { - let v = UIImageView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 28, height: 28)) - } - return v - }() - - itemLabel = { - let v = CLLabel() - v.textAlignment = .center - v.font = .t.tls - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(block.snp.bottom).offset(4) - } - return v - }() - } - - private func setupData() { - } - - private func setupEvent() { - } - - func config(type: IMMoreMenuType) { - let level = IMAIViewModel.shared.aiIMInfo?.aiUserHeartbeatRelation?.heartbeatLevel ?? .level1 - - switch type { - case .picture: - icon.image = MWIconFont.image(fromIcon: .iconUploadimg, size: CGSize(width: 28, height: 28), color: .white) - if level.isGreaterOrEqual(to: .level2){ - itemLabel.text = "Picture" - }else{ - itemLabel.text = "Lv2.unlock" - } - case .phone: - icon.image = MWIconFont.image(fromIcon: .call, size: CGSize(width: 28, height: 28), color: .white) - if level.isGreaterOrEqual(to: .level4){ - itemLabel.text = "Voice Call" - }else{ - itemLabel.text = "Lv4.unlock" - } - case .share: - icon.image = MWIconFont.image(fromIcon: .shareBorder, size: CGSize(width: 28, height: 28), color: .white) - itemLabel.text = "Share" - } - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/Input/IMVoiceHoldView.swift b/crush/Crush/Src/Modules/Chat/Session/Input/IMVoiceHoldView.swift deleted file mode 100644 index 42f6501..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/Input/IMVoiceHoldView.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// IMVoiceHoldView.swift -// Crush -// -// Created by Leon on 2025/8/22. -// - -import UIKit -import Lottie -class IMVoiceHoldView: UIView { -// var gradientBg: GradientView! - var overlayBg: UIImageView! - - var voiceWaveContainer: GradientView! - var wave : LottieAnimationView! - - var tipLabel: UILabel! - - var voiceIconDecorationView: UIImageView! - var voiceIcon: UIImageView! - - var recordTool: AudioRecordTool! - // State - var audioDuration: Int = 0 - var counting: Int = 0 - private var isRecording: Bool = false - public var audioPathUrl: URL? - - var recordFinishedAction: ((_ url: URL?) -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - isUserInteractionEnabled = false - - snp.makeConstraints { make in - make.height.equalTo(300) - } - - overlayBg = { - let v = UIImageView() - v.contentMode = .scaleToFill - v.image = UIImage(named: "voice_hold_overlay_bot") - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - -// gradientBg = { -// let colors = [ -// CLGlobalToken.color(token: .glo_color_black)!.withAlphaComponent(0), -// CLGlobalToken.color(token: .glo_color_black)!.withAlphaComponent(0.65), -// CLGlobalToken.color(token: .glo_color_black)!, -// ] -// let v = GradientView(colors: colors, gradientType: .topToBottom) -// addSubview(v) -// v.snp.makeConstraints { make in -// make.edges.equalToSuperview() -// } -// return v -// }() - - voiceWaveContainer = { - let gradient = CLSystemToken.gradient(token: .cpgn) - let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight) - v.cornerRadius = 24 - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(64) - make.leading.equalToSuperview().offset(48) - make.trailing.equalToSuperview().offset(-48) - make.height.equalTo(48) - } - -// let temp = UIImageView() -// temp.image = UIImage(named: "temp_voice_wave") -// v.addSubview(temp) -// temp.snp.makeConstraints { make in -// make.edges.equalToSuperview() -// } - - return v - }() - - wave = { - let animation = LottieAnimation.named("voice_im_recording") - let voiceAnimation = LottieAnimationView(animation: animation) - // voiceAnimation = LottieAnimationView.animationNamed("voice_white.json") - voiceAnimation.isUserInteractionEnabled = false - voiceAnimation.loopMode = .loop - voiceAnimation.animationSpeed = 1.4 - voiceAnimation.contentMode = .scaleAspectFit - voiceAnimation.tintColor = .white - voiceAnimation.play() - voiceWaveContainer.addSubview(voiceAnimation) - voiceAnimation.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalToSuperview().offset(12) - make.bottom.equalToSuperview().offset(-12) - make.width.equalTo(voiceAnimation.snp.height).multipliedBy(470/80.0) - } - voiceAnimation.isHidden = false - return voiceAnimation - }() - - tipLabel = { - let v = UILabel() - v.font = .t.tlm - v.textColor = .desc - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(voiceWaveContainer.snp.bottom).offset(24) - } - return v - }() - - voiceIconDecorationView = { - let v = UIImageView() - v.image = UIImage(named: "voice_record_decoration") - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(120/393.0) - } - return v - }() - - voiceIcon = { - let v = UIImageView() - let image = MWIconFont.image(fromIcon: .voiceMsg, size: CGSize(width: 32, height: 32), color: CLGlobalToken.color(token: .glo_color_grey_80)) - v.image = image - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(voiceIconDecorationView).offset(36) - } - return v - }() - - tipLabel.text = "Release to send" - } - - private func setupData() { - recordTool = AudioRecordTool() - } - - private func setupEvent() { - recordTool.timerChangedBlock = { [weak self] counting in - self?.counting = counting - //let seconds = CGFloat(counting) / 10.0 - // dlog("麦克风\(seconds)s") - - if counting >= 60 * 10 - 1 { - self?.stopRecord() - - return - } - } - } - - // MARK: - Public - - func record(on: Bool) { - - CLTool.feedbackGenerator() - isRecording = on - - if on { - wave.play() - recordTool.startRecord { [weak self] audioURL in - self?.audioPathUrl = audioURL - dlog("☁️🎤audioURL: \(String(describing: audioURL))") - - if let count = self?.counting, count < 10{ - Hud.toast(str: "语音时间太短") - return - } - self?.recordFinishedAction?(audioURL) - } - } else { - wave.stop() - stopRecord() - } - } - - // MARK: - Functions - - func stopRecord() { - recordTool.stopRecord() -// if counting < 10{ -// Hud.toast(str: "语音时间太短") -// return -// } - - } - - // MARK: - Other -} diff --git a/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputOperateView.swift b/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputOperateView.swift deleted file mode 100644 index e1cfa8a..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputOperateView.swift +++ /dev/null @@ -1,287 +0,0 @@ -// -// SessionInputOperateView.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import Combine -import UIKit -protocol SessionInputOperateViewDelegate: AnyObject { - func operateTapGiftAction() - func operateVoiceAction(on: Bool) - func operateTapMoreAction() - func operateTapHelpAction() - func operateTapInputFieldAction() -} - -enum InputOperateState { - case text - case voice -} - -class SessionInputOperateView: UIView { - weak var delegate: SessionInputOperateViewDelegate? - - var safeView: UIView! - var giftButton: EPIconPrimaryButton! - - var block: UIView! - var effectViewOnBlock: UIVisualEffectView! - var modeButton: UIButton! - var rightStackH: UIStackView! - var helpButton: UIButton! - var moreButton: UIButton! - - var fakeTextfield: UITextField! - var voiceHoldView: UIView! - - //var tapInputFieldAction: (() -> Void)? - // var voiceHoldAction: ((_ onVoice: Bool) -> Void)? - - /// 当前模式 - @Published var state: InputOperateState = .text - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - safeView = { - let v = UIView() - v.backgroundColor = .clear - addSubview(v) - v.snp.makeConstraints { make in - make.leading.top.trailing.equalToSuperview() - // make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom*0.5-16) - make.bottom.equalToSuperview() - } - return v - }() - - giftButton = { - let v = EPIconPrimaryButton(radius: .round, iconSize: .large, iconCode: .giftBorder) - v.addTarget(self, action: #selector(tapGiftButton), for: .touchUpInside) - safeView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - block = { - let v = UIView() - v.backgroundColor = .c.cseln - v.layer.cornerRadius = 24 - v.layer.masksToBounds = true - safeView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(giftButton.snp.trailing).offset(12) - make.trailing.equalToSuperview().offset(-24) - make.height.equalTo(48) - make.centerY.equalToSuperview() - } - return v - }() - - effectViewOnBlock = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 0.9 - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - modeButton = { - let v = UIButton() - block.addSubview(v) - let size = CGSize(width: 20, height: 20) - let image = MWIconFont.image(fromIcon: .voiceMsg, size: size, color: .white) - v.setImage(image, for: .normal) - v.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - v.addTarget(self, action: #selector(tapModeButton), for: .touchUpInside) - v.snp.makeConstraints { make in - make.size.equalTo(size) - make.leading.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - } - return v - }() - - rightStackH = { - let v = UIStackView() - v.spacing = 20 - v.alignment = .center - block.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - return v - }() - - helpButton = { - let v = UIButton() - rightStackH.addArrangedSubview(v) - let size = CGSize(width: 20, height: 20) - let image = MWIconFont.image(fromIcon: .prompt, size: size, color: .white) - v.setImage(image, for: .normal) - v.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - v.addTarget(self, action: #selector(tapHelpButton), for: .touchUpInside) - v.snp.makeConstraints { make in - make.size.equalTo(size) - } - return v - }() - - moreButton = { - let v = UIButton() - rightStackH.addArrangedSubview(v) - let size = CGSize(width: 20, height: 20) - let image = MWIconFont.image(fromIcon: .add, size: size, color: .white) - v.setImage(image, for: .normal) - v.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - v.addTarget(self, action: #selector(tapMoreButton), for: .touchUpInside) - v.snp.makeConstraints { make in - make.size.equalTo(size) - } - return v - }() - - fakeTextfield = { - let v = UITextField() - v.font = .t.tll - v.textColor = .c.ctpn - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(modeButton.snp.trailing).offset(16) - make.trailing.equalTo(rightStackH.snp.leading).offset(-16) - } -// v.text = "Chat" - v.attributedPlaceholder = "Chat".withAttributes([ - .font(.t.tll), - .textColor(UIColor.c.ctsn), - ]) - v.delegate = self - return v - }() - - voiceHoldView = { - let v = UIView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.bottom.equalToSuperview() - make.leading.trailing.equalTo(fakeTextfield) - } - - let label = UILabel() - label.font = .t.tll - label.textColor = .white - label.textAlignment = .center - label.text = "Hold to Talk" - v.addSubview(label) - label.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - } - - private func setupData() { - state = .text - } - - private func setupEvent() { - // 添加长按手势 - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) - longPress.minimumPressDuration = 0.5 // 长按时间,默认 0.5 秒 - voiceHoldView.addGestureRecognizer(longPress) - - $state.sink { [weak self] state in - self?.refreshByState(stateOf: state) - }.store(in: &cancellables) - - - } - - // MARK: - Helper - - private func refreshByState(stateOf: InputOperateState) { - let size = CGSize(width: 20, height: 20) - switch stateOf { - case .text: - fakeTextfield.isHidden = false - voiceHoldView.isHidden = true - let image = MWIconFont.image(fromIcon: .voiceMsg, size: size, color: .white) - modeButton.setImage(image, for: .normal) - case .voice: - fakeTextfield.isHidden = true - voiceHoldView.isHidden = false - let image = MWIconFont.image(fromIcon: .iconKeyboard, size: size, color: .white) - modeButton.setImage(image, for: .normal) - AudioRecordTool.audioAuth() - } - } - - // MARK: - Action - - @objc private func tapGiftButton() { - delegate?.operateTapGiftAction() - } - - @objc private func tapModeButton() { - // delegate?.operateTapVoiceAction() - if state == .text { - self.state = .voice - } else { - self.state = .text - } - } - - @objc private func tapMoreButton() { - delegate?.operateTapMoreAction() - } - - @objc private func tapHelpButton(){ - delegate?.operateTapHelpAction() - } - - @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) { - switch gesture.state { - case .began: - delegate?.operateVoiceAction(on: true) - case .changed: - break - case .ended: - delegate?.operateVoiceAction(on: false) - case .cancelled, .failed: - delegate?.operateVoiceAction(on: false) - default: - break - } - } -} - -extension SessionInputOperateView: UITextFieldDelegate { - func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - //dlog("tap fake field") - //tapInputFieldAction?() - delegate?.operateTapInputFieldAction() - return false - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputSuggestionsView.swift b/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputSuggestionsView.swift deleted file mode 100644 index 62b5d50..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputSuggestionsView.swift +++ /dev/null @@ -1,438 +0,0 @@ -// -// SessionInputSuggestionsView.swift -// Crush -// -// Created by Leon on 2025/8/26. -// -import Combine - -class SessionInputSuggestionsView: UIView { - var bgView: UIView! - - var cv: UICollectionView! - var layout = UICollectionViewFlowLayout() - - var indicator: IndicatorView! - - var pageControl: UIPageControl! - - @Published var strings: [String]? - var datas: [[String]] = [[String]]() - - var tapItemWithAction: ((_ string: String, _ edit: Bool) -> Void)? - - // Flag - var requestNewDatasNextTime = false - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csbn - - let height = 232.0 - - bgView = { - let v = UIView() - v.backgroundColor = backgroundColor - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview().offset(UIWindow.safeAreaBottom + 16) - } - return v - }() - -// snp.makeConstraints { make in -// make.height.equalTo(height) -// } - - indicator = { - let v = IndicatorView(color: .white) - v.size = CGSize(width: 24, height: 24) - addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - - cv = { - let width = UIScreen.width - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 0 - layout.scrollDirection = .horizontal - layout.sectionInset = .zero - - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.isPagingEnabled = true - cv.contentInsetAdjustmentBehavior = .never - cv.register(SessionInputSuggestionsPageCell.self, forCellWithReuseIdentifier: "SessionInputSuggestionsPageCell") - addSubview(cv) - cv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - make.height.equalTo(height) - } - return cv - }() - - pageControl = { - let v = UIPageControl() - v.numberOfPages = 0 - v.currentPage = 0 - v.pageIndicatorTintColor = .white.withAlphaComponent(0.45) - v.currentPageIndicatorTintColor = .white - v.hidesForSinglePage = true - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(cv.snp.bottom).offset(8) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom - 8) - } - return v - }() - } - - private func setupData() { - } - - private func setupEvent() { - NotificationCenter.default.addObserver(self, selector: #selector(notifyChargeDone), name: AppNotificationName.chargeDonePushTradeId.notificationName, object: nil) - - $strings.sink {[weak self] suggestions in - if (suggestions ?? []).count > 0{ - self?.indicator.isHidden = true - self?.indicator.stopIndicator() - }else{ - self?.indicator.isHidden = false - self?.indicator.startIndicator() - } - }.store(in: &cancellables) - } - - @objc private func notifyChargeDone(){ - hide() - } - - public func show(id: Int) { - isHidden = false - - if requestNewDatasNextTime || datas.count == 0{ - loadTextSuggestion(id: id) - }else{ - // 延用旧的数据 - handleTextSuggestions(suggestions: strings) - } - } - - /// 带清空显示数据 - public func hide(){ - isHidden = true - } - - func loadTextSuggestion(id: Int) { - resetToEmpty() - - AICowProvider.request(.supChatContent(aiId: id), modelType: [String].self) { [weak self] result in - switch result { - case let .success(model): - self?.strings = model - self?.handleTextSuggestions(suggestions: model) - self?.requestNewDatasNextTime = false - case .failure: - // 可能是余额不足 - self?.hide() - break - } - } - } - - private func handleTextSuggestions(suggestions:[String]?){ - datas.removeAll() - - if let strings = suggestions, !strings.isEmpty { - // Calculate number of pages needed (9 items, 3 per page = 3 pages) - let itemsPerPage = 3 - let pageCount = Int(ceil(Double(strings.count) / Double(itemsPerPage))) - - // Split strings into chunks of 3 - for i in 0 ..< pageCount { - let startIndex = i * itemsPerPage - let endIndex = min(startIndex + itemsPerPage, strings.count) - let pageData = Array(strings[startIndex ..< endIndex]) - datas.append(pageData) - } - } - cv.reloadData() - pageControl.numberOfPages = datas.count - pageControl.currentPage = 0 - } - - // MARK: - Helper - - private func resetToEmpty(){ - datas.removeAll() - strings = [] - cv.reloadData() - pageControl.currentPage = 0 - pageControl.numberOfPages = 0 - } -} - -extension SessionInputSuggestionsView: UICollectionViewDelegate, UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SessionInputSuggestionsPageCell", for: indexPath) as! SessionInputSuggestionsPageCell - let strings = datas[indexPath.item] - cell.config(strings) - cell.tapItemWithAction = { [weak self] string, edit in - self?.tapItemWithAction?(string, edit) - } - return cell - } -} - -extension SessionInputSuggestionsView: UIScrollViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let page = Int(scrollView.contentOffset.x / UIScreen.width) - pageControl.currentPage = page - } -} - -class SessionInputSuggestionsPageCell: UICollectionViewCell { - var stackV: UIStackView! - - var item1: SessionInputSuggestionsItem! - var item2: SessionInputSuggestionsItem! - var item3: SessionInputSuggestionsItem! - var coinUnlockItem: SessionInputCoinUnlockItem! - - var tapItemWithAction: ((_ string: String, _ edit: Bool) -> Void)? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - stackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 16 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - - item1 = { - let v = SessionInputSuggestionsItem() - stackV.addArrangedSubview(v) - return v - }() - - item2 = { - let v = SessionInputSuggestionsItem() - stackV.addArrangedSubview(v) - return v - }() - - item3 = { - let v = SessionInputSuggestionsItem() - stackV.addArrangedSubview(v) - v.isHidden = true - return v - }() - - coinUnlockItem = { - let v = SessionInputCoinUnlockItem() - stackV.addArrangedSubview(v) - v.isHidden = true - return v - }() - } - - func config(_ strings: [String]) { - for perItem in stackV.arrangedSubviews { - perItem.isHidden = true - } - - for (index, per) in strings.enumerated() { - if index == 0 { - item1.isHidden = false - item1.label.text = per - } else if index == 1 { - item2.isHidden = false - item2.label.text = per - } else if index == 2 { - item3.isHidden = false - item3.label.text = per - } - } - } - - private func setupData() { - } - - private func setupEvent() { - item1.tapItemWithAction = { [weak self] string, edit in - self?.tapItemWithAction?(string, edit) - } - item2.tapItemWithAction = { [weak self] string, edit in - self?.tapItemWithAction?(string, edit) - } - item3.tapItemWithAction = { [weak self] string, edit in - self?.tapItemWithAction?(string, edit) - } - } -} - -class SessionInputSuggestionsItem: UIView { - var editButton: EPIconGhostButton! - var label: CLLabel! - var topButton: UIButton! - - var tapItemWithAction: ((_ string: String, _ edit: Bool) -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.cseln - cornerRadius = 12 - snp.makeConstraints { make in - make.height.equalTo(48) - } - - editButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .iconOrderRemark) - v.addTarget(self, action: #selector(tapEditButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-8) - make.centerY.equalToSuperview() - } - return v - }() - - label = { - let v = CLLabel() - v.font = .t.tbl - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(16) - make.trailing.equalTo(editButton.snp.leading).offset(-8) - } - return v - }() - - topButton = { - let v = UIButton() - v.addTarget(self, action: #selector(tapTopButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.top.bottom.equalToSuperview() - make.leading.equalToSuperview() - make.trailing.equalTo(label) - } - return v - }() - } - - @objc private func tapTopButton() { - tapItemWithAction?(label.text ?? "", false) - } - - @objc private func tapEditButton() { - tapItemWithAction?(label.text ?? "", true) - } -} - -class SessionInputCoinUnlockItem: UIView { - var gradientView: GradientView! - var label: UILabel! - var topButton: UIButton! - override init(frame: CGRect) { - super.init(frame: frame) - - gradientView = { - let gradient = CLSystemToken.gradient(token: .ccvn) - let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - label = { - let v = UILabel() - v.textAlignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - - topButton = { - let v = UIButton() - addSubview(v) - v.snp.makeConstraints { make in - make.top.bottom.equalToSuperview() - make.leading.equalToSuperview() - make.trailing.equalTo(label) - } - return v - }() - - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: "Heart member unlock", icon: .iconVip) - label.attributedText = aStr - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputView.swift b/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputView.swift deleted file mode 100644 index dea4306..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/Input/SessionInputView.swift +++ /dev/null @@ -1,345 +0,0 @@ -// -// Untitled.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit -import Combine -protocol IMInputBarDelegate: AnyObject{ - func albumTapAction() - func sendTapAction(mesgStr: String, photo: UploadPhotoM?) - func giftTapAction() - func helpTapAction() - func moreTapAction() -} - -class SessionInputView: UIView { - weak var delegate: IMInputBarDelegate? - - var block:UIView! - - var effectView: UIVisualEffectView! - - var contentStackV: UIStackView! - var referenceButton: CommonUploadImageButton! - var textView: CLTextView! - - var leftButtonsStackH: UIStackView! - var giftButton: EPIconGhostButton! - var photoButton: EPIconGhostButton! - - var rightButtonsStackH: UIStackView! - var helpButton: EPIconGhostButton! - var addButton: EPIconGhostButton! - var sendButtonContainer : UIView! - var sendButton: EPIconPrimaryButton! - - let minTextViewHeight = 40.0//56.0 - - @Published var uploadImage: UploadPhotoM? - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .clear - - block = { - let v = UIView() - v.cornerRadius = 16 - v.layer.masksToBounds = true - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 24, bottom: 16, right: 24)) - } - return v - }() - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 0.9 - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - contentStackV = { - let v = UIStackView() - v.spacing = 12 - v.axis = .vertical - v.alignment = .leading - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(12) // 16 - } - return v - }() - - referenceButton = { - let v = CommonUploadImageButton() - contentStackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(100) - make.width.equalTo(100) - } - v.isHidden = true - return v - }() - - textView = { - let v = CLTextView() - v.placeholder = "Chat" - v.backgroundColor = .clear - v.limit.maxCharacterNumber = 500 - v.font = .t.tll - v.textColor = .text - contentStackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(12) - make.trailing.equalToSuperview().offset(-12) - make.height.equalTo(minTextViewHeight) - } - return v - }() - - leftButtonsStackH = { - let v = UIStackView() - v.spacing = 4 - v.alignment = .center - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(contentStackV.snp.bottom).offset(8) - make.leading.equalToSuperview().offset(8) - make.bottom.equalToSuperview().offset(-8) - make.height.equalTo(40) - } - return v - }() - - giftButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .giftBorder) - v.addTarget(self, action: #selector(tapGiftButton), for: .touchUpInside) - leftButtonsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - photoButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .iconUploadimg) - v.addTarget(self, action: #selector(tapPhotoButton), for: .touchUpInside) - leftButtonsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - rightButtonsStackH = { - let v = UIStackView() - v.spacing = 4 - v.alignment = .center - block.addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(contentStackV.snp.bottom).offset(8) - make.centerY.equalTo(leftButtonsStackH) - make.trailing.equalToSuperview().offset(-8) - make.height.equalTo(40) - } - return v - }() - - helpButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .prompt) - v.addTarget(self, action: #selector(tapHelpButton), for: .touchUpInside) - rightButtonsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - addButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .add) - rightButtonsStackH.addArrangedSubview(v) - v.addTarget(self, action: #selector(tapAddButton), for: .touchUpInside) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - v.isHidden = true - return v - }() - - sendButtonContainer = { - let v = UIView() - rightButtonsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSizeMake(36, 36)) - } - return v - }() - - sendButton = { - let v = EPIconPrimaryButton(radius: .round, iconSize: .small, iconCode: .iconSend) - v.addTarget(self, action: #selector(tapSendButton), for: .touchUpInside) - v.isEnabled = true - sendButtonContainer.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - make.center.equalToSuperview() - } - return v - }() - } - - private func setupData() { - } - - private func setupEvent() { - - textView.textPublisher.sink {[weak self] str in - self?.refreshButtonState() - self?.fixTextViewHeight() - }.store(in: &cancellables) - - referenceButton.$uploadState.sink {[weak self] state in - dlog("image uploadstate: \(state)") - self?.refreshButtonState() - }.store(in: &cancellables) - - referenceButton.closeAction = {[weak self] button in - self?.uploadImage = nil - button.givtStateOutsize(.success) - button.setupImageDefaultState() - button.isHidden = true - } - - $uploadImage.sink {[weak self] model in - self?.referenceButton.isHidden = model == nil - self?.refreshButtonState() - }.store(in: &cancellables) - } - - // MARK: - Public - public func fillDefaultContent(string: String){ - textView.text = string - NotificationCenter.default.post( - name: UITextView.textDidChangeNotification, - object: textView) - } - - - - func bindImage(img: UIImage){ - uploadImage = referenceButton.bindViewImage(img, checkImage: true) - - } - - func clearInputDatas(){ - self.textView.text = "" - NotificationCenter.default.post( - name: UITextView.textDidChangeNotification, - object: textView - ) - uploadImage = nil - // cancelFistResponse() - } - - // MARK: - Action - - @objc private func tapSendButton(){ - let text = textView.text.trimmed - -// #warning("test") -// let model = UploadPhotoM() -// model.remoteFullPath = "https://hhb.crushlevel.ai/static/img/dfhig.jpg" -// model.isAutoCheckImage = false -// model.addThisItemTimeStamp = Date().timeStamp -// model.imageSize = CGSize(width: 200, height: 300) -// uploadImage = model - - delegate?.sendTapAction(mesgStr: text, photo: uploadImage) - } - - @objc private func tapGiftButton(){ - delegate?.giftTapAction() - } - - @objc private func tapPhotoButton(){ - delegate?.albumTapAction() - } - - @objc private func tapHelpButton(){ - delegate?.helpTapAction() - } - - @objc private func tapAddButton(){ - delegate?.moreTapAction() - } - - // MARK: - Helper - - private func fixTextViewHeight(){ - if textView.contentSize.height > minTextViewHeight{ - textView.snp.updateConstraints { make in - make.height.equalTo(75) // 73 - } - }else{ - textView.snp.updateConstraints { make in - make.height.equalTo(textView.contentSize.height) //minTextViewHeight - } - } - } - - private func refreshButtonState(){ - // 文本发送是必须的! - let textCheckOK = (textView.text.trimmed).count > 0 - - addButton.isHidden = textCheckOK - sendButtonContainer.isHidden = !textCheckOK - - var imageCheckOK = true - if let img = uploadImage{ - imageCheckOK = false - if let imageUrl = img.remoteFullPath, imageUrl.count > 0, (img.imageChecked && img.imageCheckIsViolation == false){ - imageCheckOK = true - } - } - sendButton.isEnabled = textCheckOK && imageCheckOK - } - - public func cancelFistResponse() { - self.endEditing(true) - } - - @discardableResult - override func becomeFirstResponder() -> Bool { - self.textView.becomeFirstResponder() - } - - public func becomeFistResponse() { - self.textView.becomeFirstResponder() - } - - public func isFistReponse() -> Bool { - return textView.isFirstResponder - } - - -} diff --git a/crush/Crush/Src/Modules/Chat/Session/SessionController+Adapter.swift b/crush/Crush/Src/Modules/Chat/Session/SessionController+Adapter.swift deleted file mode 100755 index e6ef58d..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/SessionController+Adapter.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// SessionController+Adapter.swift -// LegendTeam -// -// Created by 梁博 on 20/12/21. -// - -import Foundation -import UIKit - -extension SessionController { - func setupTableView() { - tableView = UITableView(frame: .zero, style: .plain) - self.view.addSubview(tableView) - tableView.backgroundColor = .clear - tableView.delegate = self - tableView.dataSource = self - tableView.showsVerticalScrollIndicator = false - tableView.showsHorizontalScrollIndicator = false - tableView.contentInsetAdjustmentBehavior = .never - tableView.keyboardDismissMode = .onDrag - tableView.separatorStyle = .none - tableView.estimatedRowHeight = 100 - tableView.estimatedSectionFooterHeight = 0 - tableView.estimatedSectionHeaderHeight = 0 - tableView.contentInset = UIEdgeInsets(top: 40, left: 0, bottom: 20, right: 0)// UIWindow.navBarTotalHeight - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") - tableView.register(SessionAIHeadView.self, forCellReuseIdentifier: "SessionAIHeadView") - tableView.setContentCompressionResistancePriority(UILayoutPriority(743), for: .vertical) - - let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(cancelEditing)) - gestureRecognizer.numberOfTapsRequired = 1 - gestureRecognizer.cancelsTouchesInView = false - tableView.addGestureRecognizer(gestureRecognizer) - - let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTapPageBg(_:))) - doubleTap.numberOfTapsRequired = 2 // 双击 - doubleTap.numberOfTouchesRequired = 1 // 单指 - tableView.addGestureRecognizer(doubleTap) - - tableView.snp.makeConstraints { make in - make.leading.trailing.equalTo(self.view) - // Case 1: - make.top.equalTo(sessionNavigationView.snp.bottom) // navigationView - // Case 2: -// make.height.equalTo(UIScreen.height * 0.4) - make.bottom.equalTo(bottomViewsStackV.snp.top).offset(0) - } - - let header = RefreshHeaderAnimator.init(refreshingBlock: {[weak self] in - self?.loadMessages { _, _ in - - } - }) - header.ignoredScrollViewContentInsetTop = tableView.contentInset.top - tableView.mj_header = header - - tableView.alpha = 0 - } - - private func cellIn(tableView: UITableView, cellModel: SessionBaseModel) -> SessionCell { - var cellID = cellModel.cellType.rawValue - if String.realEmpty(str: cellID) { - cellID = SessionCellType.text.rawValue - } - - var cell = tableView.dequeueReusableCell(withIdentifier: cellID) - if cell != nil { - return cell as! SessionCell - } else { - tableView.register(SessionCell.self, forCellReuseIdentifier: cellID) - cell = tableView.dequeueReusableCell(withIdentifier: cellID) - return cell as! SessionCell - } - } - - func updateTableViewContentLayout() { - // 动态调整contentInset,使内容底部对齐 - adjustTableViewContentInset() - - DispatchQueue.main.async {[weak self] in - self?.scrollToBottom(self?.tableView) - } - } - - func scrollToBottom(_ tableView: UITableView?, delay: CGFloat? = 0, animated: Bool = false) { - guard let table = tableView else{ - return - } - - let delaySeconds = delay ?? 0 -// let y = max(table.contentSize.height - table.bounds.size.height, 0) -// table.setContentOffset(CGPoint(x: 0, y: y), animated: animated) - DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {[weak self] in - let count = self?.util.cellModels.count ?? 0 - if count > 0 { - table.scrollToRow(at: IndexPath(row: count - 1, section: 1), at: .bottom, animated: animated) - // Initial state - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - if table.alpha == 0{ - UIView.animate(withDuration: 0.35) { - table.alpha = 1 - } - } - } - } - } - - } -} - -// MARK: - Reload - -extension SessionController { - /// 更新某些cell - func update(indexs: [Int]?) { - guard indexs != nil else { - return - } - let indexPaths = self.indexPathsWith(indexs: indexs!) - - // 没有就不刷新 - if Array.realEmpty(array: indexPaths) { - return - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self.tableView.reloadRows(at: indexPaths, with: .none) - } - } - - /// 插入某些cell - func insert(indexs: [Int], animation: Bool, isOut: Bool, first:Bool = false) { - guard indexs.count > 0 else { - return - } - // 别人发的消息,且上滑了一定的距离才显示newMessage,需要早点计算 - let offset = self.tableView.contentSize.height - self.tableView.bounds.size.height - 80 - // 获取需要插入的indexPaths - let indexPaths = self.indexPathsWith(indexs: indexs) - let needReload = SessionUtilOC.shouldReload(whenInsert: indexPaths, tableView: self.tableView) - if needReload || !animation { - self.tableView.reloadData() - } else { - UIView.performWithoutAnimation { - self.tableView.beginUpdates() - self.tableView.insertRows(at: indexPaths, with: .none) - self.tableView.endUpdates() - } - } - // 插入第一条消息,不处理下面这个 - if first { - return - } - if isOut { - // 自己发的消息 - DispatchQueue.main.async { - self.updateTableViewContentLayout() - } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if offset > self.tableView.contentOffset.y { -// self.showNewMessages(show: true) - } else { - DispatchQueue.main.async { - self.updateTableViewContentLayout() - } - } - } - } - } - - func loadMoreCell(indexs: [Int]?) { - if Array.realEmpty(array: indexs) { - return - } - - var index = 0 - - if (self.tableView.visibleCells.count > 0) { - // 第一个一定是显示时间cell。所以要取第二个 - guard let cell = self.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? SessionCell else { - self.tableView.reloadData() - return - } - if let model = cell.model { - index = self.util.modelIndexWith(model: model) ?? 0 - } - } else { - self.tableView.reloadData() - return; - } - if (index < 0) { - index = 0; - } - - self.tableView.reloadData() - - let count = self.tableView.numberOfRows(inSection: 0) - if (index >= count && count > 0) { - index = count - 1; - } - - self.tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .top, animated: false) - } - - /// 通过index获取indexPath - func indexPathsWith(indexs: [Int]) -> [IndexPath] { - var indexPaths = [IndexPath]() - - for index in indexs { - let indexPath = IndexPath(row: index, section: 0) - indexPaths.append(indexPath) - } - return indexPaths - } -} - -// MARK: - UITableViewDataSource - -extension SessionController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return 2 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0{ - guard aiInfo != nil else{ - return 0 - } - return 1 - } - return self.util.cellModels.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section == 0{ - let cell = tableView.dequeueReusableCell(withIdentifier: "SessionAIHeadView", for: indexPath) as! SessionAIHeadView - cell.refresh(self.aiInfo) - return cell - } - let model = self.util.cellModels[indexPath.row] - let cell = self.cellIn(tableView: tableView, cellModel: model) - cell.delegate = self - cell.bindCell(model: model) - return cell - } -} - -// MARK: - UITableViewDelegate - -extension SessionController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if indexPath.section == 0{ - return UITableView.automaticDimension - } - - let model = self.util.cellModels[indexPath.row] - let cellHeight = model.cellHeight() - return cellHeight - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - //... - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let offsetY = scrollView.contentOffset.y - let contentHeight = scrollView.contentSize.height - let height = scrollView.frame.size.height - - if offsetY > contentHeight - height + 40 { - //print("松手时,滑到底部之外了") - if isPullingToAIHomePage == false{ - CLTool.feedbackGenerator() - AppRouter.goAIRoleHome(aiId: aiId) - - isPullingToAIHomePage = true - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {[weak self] in - self?.isPullingToAIHomePage = false - } - } else if offsetY < 0 { - //print("松手时,滑到顶部之外了") - } - } - - /// 动态调整tableView的contentInset,使内容底部对齐 - func adjustTableViewContentInset() { - guard let tableView = self.tableView else { return } - - // 计算内容高度 - let contentHeight = tableView.contentSize.height - let tableViewHeight = tableView.bounds.height - - // 如果内容高度小于tableView高度,调整top inset使内容底部对齐 - if contentHeight < tableViewHeight { - let topInset = tableViewHeight - contentHeight - tableView.contentInset = UIEdgeInsets(top: topInset, left: 0, bottom: 20, right: 0) - } else { - // 内容高度超过tableView时,恢复原始top inset - tableView.contentInset = UIEdgeInsets(top: 40, left: 0, bottom: 20, right: 0) - } - - // 更新下拉刷新组件的ignoredScrollViewContentInsetTop - if let header = tableView.mj_header as? RefreshHeaderAnimator { - header.ignoredScrollViewContentInsetTop = tableView.contentInset.top - } - } -} - - - diff --git a/crush/Crush/Src/Modules/Chat/Session/SessionController+Event.swift b/crush/Crush/Src/Modules/Chat/Session/SessionController+Event.swift deleted file mode 100755 index 393917f..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/SessionController+Event.swift +++ /dev/null @@ -1,641 +0,0 @@ -// -// SessionController+Event.swift -// LegendTeam -// -// Created by 梁博 on 23/12/21. -// - -import Foundation -import UIKit -import URLNavigator -import NIMSDK - -extension SessionController: SessionCellDelegate { - func setupEvent() { - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChanged(noti:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(menuHide(noti:)), name: UIMenuController.didHideMenuNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyChatSettingUpdated), name: AppNotificationName.chatSettingUpdated.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifiyRelationHiddenUpdate), name: AppNotificationName.heartbeatRelationHiddenUpdate.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifiyRelationInfoUpdate), name: AppNotificationName.aiRoleRelationInfoUpdated.notificationName, object: nil) - - - sessionNavigationView.navigationView.tapBackButtonAction = { [weak self] in - self?.close() - } - sessionNavigationView.naviMoreButton.addTarget(self, action: #selector(tapNaviMore(sender:)), for: .touchUpInside) - sessionNavigationView.likeView.likeButton.addTarget(self, action: #selector(tapLikeButton), for: .touchUpInside) - sessionNavigationView.heartTopButton.addTarget(self, action: #selector(tapHeartBeatTop), for: .touchUpInside) - sessionNavigationView.newPeopleAvatarTopButton.addTarget(self, action: #selector(tapAIHead), for: .touchUpInside) - sessionNavigationView.aiAvatarTopButton.addTarget(self, action: #selector(tapAIHead), for: .touchUpInside) - sessionNavigationView.mineAvatarTopButton.addTarget(self, action: #selector(tapMineHead), for: .touchUpInside) - - let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTapPureBg(_:))) - doubleTap.numberOfTapsRequired = 2 // 双击 - doubleTap.numberOfTouchesRequired = 1 // 单指 - view.addGestureRecognizer(doubleTap) - } - - func longPressAction(model: SessionBaseModel?, cell: SessionCell?) { - guard let theCell = cell else { return } - // 长按 - guard model?.cellType == .text else { - return - } - becomeFirstResponder() - - showCopyItem(cell: theCell) - } - - func showCopyItem(cell: SessionCell) { - let copyItem = UIMenuItem(title: "Copy", action: #selector(copyAction)) - let menuVC = UIMenuController.shared - menuVC.menuItems = [copyItem] - menuVC.showMenu(from: cell.containerView, rect: cell.containerView.bounds) - menuVC.setMenuVisible(true, animated: true) - - menuCell = cell - } - - func retryAction(model: SessionBaseModel?) { - guard let message = model?.v2msg else { return } - - if message.messageStatus.errorCode == 20000{ - if (WalletCore.shared.balance.balance ?? 0) < 100{ - //CLPurchase.shared.showIAPBuyCoinSheet() - IMAIViewModel.shared.showChatModelInsufficentCoinSheet() - return - } - }else if message.messageStatus.errorCode == 20001{ - if let heartbeatLevel = aiInfo?.aiUserHeartbeatRelation?.heartbeatLevel, heartbeatLevel.isGreaterOrEqual(to: HeartbeatLevel.level2){ - // Do nothing - }else{ - AppRouter.goAIHeartBeatLevelPage(aiId: self.aiId) - return - } - } - - if message.isSelf{ - util.sendMessage(message: message) - }else{ - // Fetch? - } -// -// if let localExt = message.localExt as? [String: Any] { -// if let code = localExt["code"] as? String { -// if code == "20000" { -// // 发送上限 -//// return -// } -// } -// } -// if message.isReceivedMsg { -// // 重新接收 -// try? NIMSDK.shared().chatManager.fetchMessageAttachment(message) -// } else { -// // 重新发送 -// try? NIMSDK.shared().chatManager.resend(message) -// } - } - - /// cell事件,可能是非点击 - func onTapCellAction(event: IMEventModel?) { - guard let theEvent = event else { return } - cancelEditing() - - switch theEvent.eventType { - case .image: - tapCellImage(event: theEvent) - case .contentUrl: - dealContentUrl(event: theEvent) - case .contactUS: - break - case .phonecallTap: - restartPhoneCall(event: theEvent) - case .aiMsgLongPress: - showAIMsgLongPressPopover(event: theEvent) - case .playAITextToAudio: - dealPlayAIMsgAudio(event: theEvent) - default: - break - } - } -} - -extension SessionController { - - // MARK: - Functions - func tapCellImage(event: IMEventModel) { - guard let tagModel = event.cellModel else{ - return - } - guard let attchment = tagModel.baseRemoteInfo?.customAttachment else{ - showCellImage(tagModel: tagModel) - return - } - // Tap the picture that need unlock! - if let price = attchment.unlockPrice, price > 0{ - var params = [String: Any]() - params.updateValue(aiId!, forKey: "aiId") - if let albumId = attchment.albumId{ - params.updateValue(albumId, forKey: "albumId") - } - if let messageServerId = tagModel.v2msg?.messageServerId{ - params.updateValue(messageServerId, forKey: "messageServerId") - } - Hud.showIndicator() - ChatProvider.request(.viewAIUnlockAlbumImg(params: params), modelType: AIAlbumUnlockImages.self, autoShowErrMsg: false) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - // 1. 正常查看图片。替换price - guard let img1 = model?.img1 else{ - return - } - let photoModel = PhotoBrowserModel() - photoModel.imageUrl = img1 - if let imageView = event.senderView as? UIImageView{ - photoModel.placeHolder = imageView.image - photoModel.sourceRect = PhotoBrowserModel.getViewRectForScreen(with: imageView) - } - ImageBrowser.show(models: [photoModel], index: 0, type: .roleOthersInIm) - tagModel.baseRemoteInfo?.customAttachment?.url = img1 - tagModel.baseRemoteInfo?.customAttachment?.unlockPrice = 0 - self?.tableView.reloadData() - - case .failure(let error): - switch error { - case let .serviceError(code, _): - if code == .imageBrowseButUnlock{ - // 大图浏览未解锁照片、去解锁 - guard let self = self else {return} - let photoModel = PhotoBrowserModel() - photoModel.sessionModel = tagModel - photoModel.aiId = self.aiId! - photoModel.imageUrl = attchment.url - if let imageView = event.senderView as? UIImageView{ - photoModel.placeHolder = imageView.image - photoModel.sourceRect = PhotoBrowserModel.getViewRectForScreen(with: imageView) - } - ImageBrowser.show(models: [photoModel], index: 0, type: .roleOthersInIm) - - self.tableView.reloadData() - } - default: - break - - } - } - } - }else{ - showCellImage(tagModel: tagModel) - } - } - - func showCellImage(tagModel: SessionBaseModel?) { - - var index: Int = 0 - var photoModels: [PhotoBrowserModel] = [PhotoBrowserModel]() - for theCell in tableView.visibleCells { - guard let cell = theCell as? SessionCell else { continue } - let cellModel = cell.model - guard let message = cellModel?.v2msg, tagModel === cellModel else { - continue - } - // image类型 - if cellModel?.cellType == .image { - let photoModel = PhotoBrowserModel() - //photoModel.message = message - - let info = IMRemoteUtil.dealRemoteInfo(message: message) - photoModel.imageUrl = info.customAttachment?.url - if let contentView = cell.containerView as? IMImageContentView { - // 获取占位图片和区域 - if let image = contentView.imageView.image { - photoModel.image = image - photoModel.placeHolder = image - } - photoModel.sourceRect = PhotoBrowserModel.getViewRectForScreen(with: contentView.imageView) - } - - if tagModel === cellModel { - index = photoModels.count - } - photoModels.append(photoModel) - } - } - - ImageBrowser.show(models: photoModels, index: index, type: .roleOthersInIm) - } - - func dealContentUrl(event: IMEventModel) { - guard var urlString = event.linkData else { return } - - if urlString.hasPrefix(AppConst.schemePrefix) { - navigator.open(urlString) - return - } - - if !urlString.contains("://") { - urlString = "https://\(urlString)" - } - guard let url = URL(string: urlString) else { return } - - dlog("⚠️tap url: \(url)") - } - - func restartPhoneCall(event: IMEventModel) { -// let model = event.cellModel -// headPhoneCallGuideTapCall() - } - - func showAIMsgLongPressPopover(event: IMEventModel) { - guard let sender = event.senderView else{ - return - } - guard let msg = event.cellModel?.v2msg else{ - return - } - - let createTimestamp = msg.createTime - let msgDate = Date(seconds: createTimestamp) - var okToLikeOrDislike = true - if Date().hours(from: msgDate) > 24 * 7{ - okToLikeOrDislike = false - } - - let remoteInfo = IMRemoteUtil.dealRemoteInfo(message: msg) - - let pop = CLPopoverListView() - let rect = view.convert(sender.frame, from: sender.superview) - var items = [CLPopoverListTextItem]() - do { - let listItem = CLPopoverListTextItem() - listItem.title = "Copy" - listItem.image = MWIconFont.image(fromIcon: .copy, size: CGSize(width: 20, height: 20), color: .text) - listItem.updateLayout() - listItem.selectedHandler = { _ in - guard let msg = event.cellModel?.v2msg else{ - return - } - let string = msg.text ?? "" - UIPasteboard.general.string = string - Hud.toast(str:"Copy success") - } - items.append(listItem) - } - if okToLikeOrDislike { - let listItem = CLPopoverListTextItem() - listItem.title = "Like" - if let optType = remoteInfo.optType, optType == .upvote{ - listItem.image = MWIconFont.image(fromIcon: .postRecommendFill, size: CGSize(width: 20, height: 20), color: .c.cpn) - }else{ - listItem.image = MWIconFont.image(fromIcon: .iconPostRecommend, size: CGSize(width: 20, height: 20), color: .text) - } - listItem.updateLayout() - listItem.selectedHandler = {[weak self] _ in - self?.doLikeAIMessage(msg: msg) - } - items.append(listItem) - } - - if okToLikeOrDislike { - let listItem = CLPopoverListTextItem() - listItem.title = "Dislike" - if let optType = remoteInfo.optType, optType == .downvote{ - listItem.image = MWIconFont.image(fromIcon: .postNotrecommendFill, size: CGSize(width: 20, height: 20), color: .c.cpn) - }else{ - listItem.image = MWIconFont.image(fromIcon: .iconPostNotrecommend, size: CGSize(width: 20, height: 20), color: .text) - } - listItem.updateLayout() - listItem.selectedHandler = {[weak self] _ in - self?.doDislikeAIMessage(msg: msg) - } - items.append(listItem) - } - pop.setupCommonPopover(rect, inView: view, items: items, block: nil) - } - - private func doLikeAIMessage(msg: V2NIMMessage){ - let remoteInfo = IMRemoteUtil.dealRemoteInfo(message: msg) - if let optType = remoteInfo.optType, optType == .upvote{ - doOptAIMessage(msg: msg, opt: .unknow) - return - } - doOptAIMessage(msg: msg, opt: .upvote) - } - - private func doDislikeAIMessage(msg: V2NIMMessage){ - let remoteInfo = IMRemoteUtil.dealRemoteInfo(message: msg) - if let optType = remoteInfo.optType, optType == .downvote{ - doOptAIMessage(msg: msg, opt: .unknow) - return - } - - doOptAIMessage(msg: msg, opt: .downvote) - } - - private func doOptAIMessage(msg: V2NIMMessage, opt: MsgOptType){ - var params = [String : Any]() - - let theAIId = aiId ?? 0 - - params.updateValue(theAIId, forKey: "aiId") - if let messageServerId = msg.messageServerId, messageServerId.count > 0{ - params.updateValue(messageServerId, forKey: "messageId") - }else{ - params.updateValue("prologue_\(theAIId)", forKey: "messageId") - } - params.updateValue(msg.text ?? "", forKey: "content") - params.updateValue(opt.rawValue, forKey: "optType") - Hud.showIndicator() - IMProvider.request(.aiUserMsgFeedback(params: params), modelType: EmptyModel.self) { result in - Hud.hideIndicator() - switch result { - case .success: - break - case .failure: - break - } - } - } - - private func dealPlayAIMsgAudio(event:IMEventModel){ - let model = event.cellModel - model?.prepareAudio (info: aiInfo) { model in - if let speechModel = model{ - if let view = event.senderView as? IMAIMsgContentView{ - view.refreshModel(model: view.model) //refresh StateChange - } - SpeechManager.shared.startPlay(with: speechModel) - }else{ - // Nothing - } - } - } - - func letAISendPrologue(){ -// #warning("to do, 此处的相关逻辑,需要换成本地逻辑") -// ChatProvider.request(.letAISendPrologue(aiId: userId!), modelType: EmptyModel.self) { _ in -// -// } - guard let dialoguePrologue = aiInfo?.dialoguePrologue else {return} - - if aiInfo?.isDelChatted.boolValue == false{ - // 本地添加一条开场白消息 - let msg = IMMessageMaker.msgWithText(dialoguePrologue) - util.appendWith(messages: [msg]) - tableView.reloadData() - } - } - - - // MARK: - Action - - @objc func tapNaviMore(sender: UIButton) { - // sessionNavigationView.upDownNoticeView.showUnlocked(string: "XY") - let vc = ChatSettingListController() - vc.aiId = aiId - navigationController?.pushViewController(vc, animated: true) - } - - @objc func tapLikeButton(){ -// guard let user = userInfo else{return} -// let liked = user.liked ?? false -// let likedNum = user.likedNum ?? 0 -// - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} - - guard let likeView = sessionNavigationView.likeView else{return} - // 防止重复点击 - guard !isRequesting else { return } - guard let user = aiInfo, let aiId = user.aiId else { return } - - let isliked = user.liked.boolValue - let likedCount = user.likedNum ?? 0 - - isRequesting = true - - // 禁用按钮 - likeView.likeButton.isEnabled = false - - if isliked { - // 取消点赞 - let likeNewStatus = LikeOrCancelStatus.cancel - let newLikedCount = max(likedCount - 1, 0) - - // 立即更新UI状态 - aiInfo?.liked = false - aiInfo?.likedNum = newLikedCount - sessionNavigationView.config(user: aiInfo) - likeView.isLike = false - likeView.countLabel.text = String.displayNumber(NSNumber(value: newLikedCount), scale: 1) - - AIRoleProvider.request(.aiUserLikeOrCancel(aiId: aiId, likedStatus: likeNewStatus), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, aiId: aiId, isLike: false, originalLikedCount: likedCount) - } - } else { - // 点赞 - let likeNewStatus = LikeOrCancelStatus.liked - let newLikedCount = likedCount + 1 - - // 播放点赞动画 - likeView.playLotteLike { [weak self] completed in - // 立即更新UI状态 - self?.aiInfo?.liked = true - self?.aiInfo?.likedNum = newLikedCount - self?.sessionNavigationView.config(user: self?.aiInfo) - likeView.isLike = true - likeView.countLabel.text = String.displayNumber(NSNumber(value: newLikedCount), scale: 1) - } - - AIRoleProvider.request(.aiUserLikeOrCancel(aiId: aiId, likedStatus: likeNewStatus), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, aiId: aiId, isLike: true, originalLikedCount: likedCount) - } - } - } - // 统一处理请求结果 - private func handleLikeRequestResult(result: Result, aiId: Int, isLike: Bool, originalLikedCount: Int) { - isRequesting = false - guard let likeView = sessionNavigationView.likeView else{return} - - likeView.likeButton.isEnabled = true - - switch result { - case .success: - // 请求成功,无需额外处理 - break - case .failure: - // 请求失败,恢复状态 - if let currentAiId = self.aiInfo?.aiId, currentAiId == aiId { - if isLike { - // 点赞失败,恢复为未点赞状态 - aiInfo?.liked = false - aiInfo?.likedNum = originalLikedCount - likeView.isLike = false - likeView.countLabel.text = String.displayNumber(NSNumber(value: originalLikedCount), scale: 1) - } else { - // 取消点赞失败,恢复为点赞状态 - aiInfo?.liked = true - aiInfo?.likedNum = originalLikedCount - likeView.isLike = true - likeView.countLabel.text = String.displayNumber(NSNumber(value: originalLikedCount), scale: 1) - } - } - sessionNavigationView.config(user: aiInfo) - } - // 刷新统计 - - } - - @objc func tapHeartBeatTop() { - let vc = HeartBeatLevelGridController() - vc.aiId = aiId - navigationController?.pushViewController(vc, animated: true) - } - - @objc func tapAIHead() { - AppRouter.goAIRoleHome(aiId: aiId) - } - - @objc func tapMineHead() { - AppRouter.goBackRootController(jumpIndex: .me) - } - - @objc func handleDoubleTapPageBg(_ gesture: UITapGestureRecognizer) { - let location = gesture.location(in: tableView) - - // 获取点击位置的 indexPath(如果点击在 cell 上) - if let indexPath = tableView.indexPathForRow(at: location) { - print("双击了第 \(indexPath.section) 组,第 \(indexPath.row) 行") - } else { - print("双击在空白区域") - } - - doDoubleTabBg() - } - - @objc private func handleDoubleTapPureBg(_ gesture: UITapGestureRecognizer) { - leavePureMode() - } - - private func doDoubleTabBg(){ - if pureBgOperateView == nil{ - pureBgOperateView = { - let v = SessionPureBgOperateView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - } - v.isHidden = true - return v - }() - pureBgOperateView.backButton.addTap {[weak self] _ in - self?.doDoubleTabBg() - } - - pureBgOperateView.shareButton.addTap {[weak self] _ in - self?.tapShare() - } - - pureBgOperateView.middleButton.addTap {[weak self] _ in - self?.tapSetChatBackgroud() - } - } - - if pureBgOperateView.isHidden == true{ - enterPureMode() - }else{ - leavePureMode() - } - } - - private func enterPureMode(){ - // 进入纯净模式 - pureBgOperateView.isHidden = false - - sessionNavigationView.isHidden = true - bottomViewsStackV.isHidden = true - tableView.isHidden = true - - view.endEditing(true) - hideOperateView() - } - - private func leavePureMode(){ - if pureBgOperateView != nil{ - pureBgOperateView.isHidden = true - } - - sessionNavigationView.isHidden = false - bottomViewsStackV.isHidden = false - tableView.isHidden = false - } - - func tapShare(){ - guard let aiId = aiId else{return} - let content = "Come to Crushlevel for chat, Crush, and AI - chat." - let urlString = "\(AppConst.h5urlRoot)/@\(aiId)" - - guard let url = URL(string: urlString) else { return } - - // 分享内容(文字 + 链接) - let items: [Any] = [url, content] - - let activityVC = UIActivityViewController(activityItems: items, - applicationActivities: nil) - - // iPad 需要设置弹出位置,否则会崩溃 - if let popover = activityVC.popoverPresentationController { - popover.sourceView = view - popover.sourceRect = CGRect(x: view.bounds.midX, - y: view.bounds.midY, - width: 0, height: 0) - popover.permittedArrowDirections = [] - } - - present(activityVC, animated: true, completion: nil) - } - - private func tapSetChatBackgroud(){ - guard let aiId = aiId else{return} - let vc = ChatBackgroundGridController() - vc.aiId = aiId - - let navc = CLNavigationController(rootViewController: vc) - navc.modalPresentationStyle = .fullScreen - present(navc, animated: true) - - // 红点 - var config = AppCache.fetchCache(key: CacheKey.chatRedBadgeConfig.rawValue, type: ChatSettingCacheConfig.self) ?? ChatSettingCacheConfig() - if !config.chatBackgroundTapped{ - config.chatBackgroundTapped = true - AppCache.cache(key: CacheKey.chatRedBadgeConfig.rawValue, value: config) - NotificationCenter.post(name: .chatNaviMoreRedDotChanged) - } - } -} - -// 长安菜单 -extension SessionController { - override var canBecomeFirstResponder: Bool { - return true - } - - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - guard let items = UIMenuController.shared.menuItems else { return false } - for item in items { - if action == item.action { - return true - } - } - return false - } - - @objc func copyAction() { - guard let cell = menuCell else { return } - guard let contentView = cell.containerView as? IMTextContentView else { return } - guard let text = contentView.contentLabel.text else { return } - UIPasteboard.general.string = text - Hud.toast(str: "Copy successful") - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/SessionController+Input.swift b/crush/Crush/Src/Modules/Chat/Session/SessionController+Input.swift deleted file mode 100755 index 0b4f7f4..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/SessionController+Input.swift +++ /dev/null @@ -1,454 +0,0 @@ -// -// SessionController+Input.swift -// LegendTeam -// -// Created by 梁博 on 24/12/21. -// - -import Foundation -import UIKit -import NIMSDK -import TZImagePickerController - -extension SessionController { - func setupInputView() { - - bottomViewsStackV = { - let v = UIStackView() - v.axis = .vertical - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - } - return v - }() - - inputBar = { - let v = SessionInputView() - v.delegate = self - bottomViewsStackV.addArrangedSubview(v) - v.isHidden = true - return v - }() - - - inputEntrance = { - let v = SessionInputOperateView() - v.delegate = self - bottomViewsStackV.addArrangedSubview(v) - return v - }() - - moreView = { - let v = IMMoreItemView(frame: CGRectMake(0, 0, UIScreen.width, IMMoreItemView.heightOfMoreItemView)) - v.delegate = self - bottomViewsStackV.addArrangedSubview(v) - v.isHidden = true - return v - }() - - voiceHoldView = { - let v = IMVoiceHoldView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - } - v.layer.zPosition = 100 - v.isHidden = true - return v - }() - - giftSendView = { - let v = GiftGridSendView() - v.delegate = self - bottomViewsStackV.addArrangedSubview(v) - v.isHidden = true - return v - }() - - textSuggestionsView = { - let v = SessionInputSuggestionsView() - bottomViewsStackV.addArrangedSubview(v) - v.isHidden = true - return v - }() - - overlay.snp.remakeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.bottom.equalTo(inputEntrance.snp.bottom).offset(UIWindow.safeAreaBottom) - } - - voiceHoldView.recordFinishedAction = {[weak self] url in - self?.doSendVoice(url: url) - } - - textSuggestionsView.tapItemWithAction = {[weak self] string , edit in - guard string.count > 0 else {return} - self?.textSuggestionsView.isHidden = true - if edit { - // 进入编辑框 - self?.inputBar.fillDefaultContent(string: string) - self?.inputBar.becomeFirstResponder() - }else{ - let msg = IMMessageMaker.msgWithText(string) - self?.util.sendMessage(message: msg) - } - } - - inputBar.textView.textPublisher.sink {[weak self] string in - self?.inputEntrance.fakeTextfield.text = string - }.store(in: &cancellables) - } - - // MARK: - Helper - - - func hideAllBottomViews(except:[UIView]? = nil){ - let viewsExcept = except ?? [] - for view in bottomViewsStackV.arrangedSubviews { - // 如果在 except 列表中,就保持显示,否则隐藏 - view.isHidden = !viewsExcept.contains(view) - } - } - - func hideOperateView(){ - hideAllBottomViews(except: [inputEntrance]) - } - - // MARK: - Functions - private func doSendVoice(url: URL?){ - // ⚠️海外和大陆接口方式不同 - //asrWay1(url: url) - - asrWay2(url: url) - } - - private func asrWay1(url: URL?){ - guard let voiceString = CLTool.m4aFileToBase64(fileURL: url) else{ - return - } - - Hud.showIndicator() - AICowProvider.request(.voiceAsr(voiceBase64: voiceString), modelType: String.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - dlog("☁️🎤:\(String(describing: model))") - if let string = model, string.count > 0{ - let msg = IMMessageMaker.msgWithText(string) - self?.util.sendMessage(message: msg) - } - case .failure: - break - } - } - } - - private func asrWay2(url: URL?){ - guard let pathInSandbox = url else{ - return - } - guard let aiId = aiId else {return} - - do { - - let data = try Data(contentsOf: pathInSandbox) - // ✅ data 就是文件的二进制数据 - let model = UploadModel() - model.addThisItemTimeStamp = Date().timeStamp - model.fileData = data - Hud.showIndicator() - CloudStorage.shared.s3AddUploadAudio(model) { uploadResult in - if uploadResult{ - guard let remoteFullPath = model.remoteFullPath else{ - assert(false) - return - } - AICowProvider.request(.voiceAsr2(url: remoteFullPath, aiId: aiId), modelType: AudioAsrResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - if let response = model, let string = response.content, string.count > 0{ - let msg = IMMessageMaker.msgWithText(string) - self?.util.sendMessage(message: msg) - } - case .failure: - break - } - } - }else{ - Hud.hideIndicator() - } - } - } catch { - print("❌读取文件audio mp3失败: \(error)") - } - } - - - func dealWillSendMessage(message: V2NIMMessage) -> Bool { - return IMManager.dealWillSendMessage(message: message) - } -} - -// MARK: - SessionInputOperateViewDelegate -extension SessionController: SessionInputOperateViewDelegate{ - func operateTapGiftAction() { - - view.endEditing(true) - - if giftSendView.isHidden{ - hideAllBottomViews(except: [inputEntrance, giftSendView]) - giftSendView.targetHeartbeatValue = aiInfo?.aiUserHeartbeatRelation?.heartbeatVal ?? 0 - }else{ - hideAllBottomViews(except: [inputEntrance]) - } - - } - - func operateVoiceAction(on: Bool){ - guard AudioRecordTool.audioAuth(showAlert: on) else{ - return - } - - voiceHoldView.isHidden = !on - voiceHoldView.record(on: on) - } - - func operateTapMoreAction() { - view.endEditing(true) - - if moreView.isHidden{ - hideAllBottomViews(except: [inputEntrance, moreView]) - }else{ - hideAllBottomViews(except: [inputEntrance]) - } - } - - func operateTapHelpAction(){ - view.endEditing(true) - - if textSuggestionsView.isHidden == true{ - hideAllBottomViews(except: [inputEntrance, textSuggestionsView]) - textSuggestionsView.show(id: aiId) - }else{ - hideAllBottomViews(except: [inputEntrance]) - } - // bottomViewsStackV.layoutIfNeeded() - } - - func operateTapInputFieldAction(){ - inputBar.becomeFirstResponder() - - hideAllBottomViews(except: [inputBar]) - // scrollToBottom(tableView, delay: 0.35, animated: true) - } -} - -// MARK: - IMInputBarDelegate - -extension SessionController: IMInputBarDelegate { - - func albumTapAction(){ - doPickSendImage() - } - - func giftTapAction() { - operateTapGiftAction() - } - - func helpTapAction(){ - operateTapHelpAction() - } - - - func sendTapAction(mesgStr: String, photo: UploadPhotoM? = nil) { - if let photoModel = photo{ - - var attachment = IMCustomAttachment() - attachment.type = .IMAGE - attachment.url = photoModel.remoteFullPath - attachment.width = FlexibleCGFloat(photoModel.imageSize.width) - attachment.height = FlexibleCGFloat(photoModel.imageSize.height) - - guard let jsonString = CodableHelper.encodeToJSONString(attachment) else{ - assert(false) - return - } - // dlog("当前json:\(String(describing: jsonString))") - - let message = IMMessageMaker.msgWithCustom(mesgStr, attachMent: jsonString) - util.sendMessage(message: message) - }else{ - if mesgStr.trimmed.isEmpty { - assert(false) - return - } - let message = IMMessageMaker.msgWithText(mesgStr) - let canSend = dealWillSendMessage(message: message) - if canSend { - util.sendMessage(message: message) - } - } - - inputBar.clearInputDatas() - } - - func moreTapAction(){ - operateTapMoreAction() - } - -} - -extension SessionController : IMMoreItemViewDelegate{ - func imMoreItemView(view: IMMoreItemView, tapType: IMMoreMenuType) { - switch tapType { - case .picture: - doPickSendImage() - case .phone: - tryStartCall() - case .share: - tapShare() - } - } - - private func doPickSendImage(){ - guard let picker = ImagePicker(maxImagesCount: 1, delegate: self) else { return } - present(picker, animated: true, completion: nil) - } - - private func tryStartCall(){ - guard AudioRecordTool.audioAuth() else{ - return - } - - guard let user = aiInfo, let levelNow = user.aiUserHeartbeatRelation?.heartbeatLevel else { - AppRouter.goAIHeartBeatLevelPage(aiId: aiId) - return - } - if levelNow.isGreaterOrEqual(to: .level4){ - doStartCall() - }else{ - AppRouter.goAIHeartBeatLevelPage(aiId: aiId) - } - } - - private func doStartCall(){ - let vc = PhoneCallController() - presentNaviRootVc(vc: vc) - vc.callEventAction = {[weak self] callType, durationInMinistamp in - guard let self = self else{return} - switch callType{ - case .CALL_CANCEL: - var model = IMCustomAttachment() - model.type = .CALL - model.callType = .CALL_CANCEL - let msg = IMMessageMaker.msgWithCustom(attachment: model) - util.sendMessage(message: msg) - case .CALL_END: - /* - var model = IMCustomAttachment() - model.type = .CALL - model.callType = .CALL_END - model.duration = durationInMinistamp - let msg = IMMessageMaker.msgWithCustom(attachment: model) - util.sendMessage(message: msg) - */ - // 此消息改为由后端发送 - break - } - } - } - - private func doShare(){ - Hud.comingsoon() - } -} - -// MARK: - GiftGridSendViewDelegate -extension SessionController: GiftGridSendViewDelegate{ - func giftSend(giftId: Int,gift: GiftDictModel, num: Int) { - guard let aiId = aiId else{ - return - } - var params = [String: Any]() - - params.updateValue(aiId, forKey: "aiId") - params.updateValue(giftId, forKey: "giftId") - params.updateValue(num, forKey: "num") - params.updateValue("IM", forKey: "scene") - - Hud.showIndicator() - ChatProvider.request(.aiUserGiftSend(params: params), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.giftSendView.reset() - // self?.giftSendView.isHidden = true - - WalletCore.shared.refreshWallet() - case .failure: - break - } - } - } - - func giftGoHeartbeatPage(){ - guard let aiId = aiId else{ - return - } - // hideAllBottomViews() - AppRouter.goAIHeartBeatLevelPage(aiId: aiId) - } - - func giftGoVIPCenter(){ - AppRouter.goVIPCenter() - } - - func giftNumPick(num: Int) { - guard let window = UIWindow.getTopDisplayWindow() else { return } // 🔥 Assuming topDisplayWindow is a custom UIView extension method - - let rect1 = giftSendView.countImg.convert(giftSendView.countImg.frame, from: giftSendView.countImg.superview) - let rect2 = giftSendView.countImg.convert(rect1, to: window) - - let vc = GiftCountChooseController(sourceRect: rect2, selectedValue: "\(num)") - - vc.chooseBlock = { [weak self] count in - // ... Handle count selection - self?.giftSendView.num = Int(count) ?? 1 - } - - vc.popoverDismiss = { [weak self] in - // ... Handle popover dismissal - self?.giftSendView.countImg.transform = .identity - - } - - addChild(vc) - view.addSubview(vc.view) - - UIView.animate(withDuration: 0.2) { - self.giftSendView.countImg.transform = CGAffineTransform(rotationAngle: .pi) - } - } - - -} - -extension SessionController : TZImagePickerControllerDelegate{ - func imagePickerController(_: TZImagePickerController!, - didFinishPickingPhotos photos: [UIImage]!, - sourceAssets _: [Any]!, - isSelectOriginalPhoto _: Bool, - infos _: [[AnyHashable: Any]]!) - { - guard let img = photos.first else { - return - } - - inputBar.becomeFirstResponder() - inputBar.bindImage(img: img) - } -} - diff --git a/crush/Crush/Src/Modules/Chat/Session/SessionController+NIM.swift b/crush/Crush/Src/Modules/Chat/Session/SessionController+NIM.swift deleted file mode 100755 index 5a3e702..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/SessionController+NIM.swift +++ /dev/null @@ -1,240 +0,0 @@ -// -// SessionController+NIM.swift -// LegendTeam -// -// Created by 梁博 on 20/12/21. -// - -import Foundation -import NIMSDK - -extension SessionController { - func setupNIM() { - NIMSDK.shared().v2MessageService.add(self) - } - - func loadMessages(completion: @escaping (( _ messageDatas: [V2NIMMessage]?, _ error: V2NIMError?) -> Void)){ - util.loadMessages {[weak self] firstPage, messageDatas, error in - - self?.tableView.mj_header?.endRefreshing() - self?.tableView.reloadData() - completion(messageDatas, error) - - // Log - self?.util.logAllCellModels() - - if error == nil { - if let nimMsgs = messageDatas { - if nimMsgs.count < 50 && nimMsgs.count >= 0{ // 说明加载完毕 - self?.dealFirstMessage() - } - - if firstPage == true, nimMsgs.count == 0{ - self?.letAISendPrologue() - } - } - - - } - } - } - - /// 清理未读 - func markReadAll() { - - IMManager.shared.clearUnreadCountBy(ids: [conversationId]) - } - - // MARK: - Functions - - /// 处理收到的消息, 处理一些特殊业务逻辑。主要考扩展字段带来的类型 - private func dealRecvMessagesEvents(messages: [V2NIMMessage]) { - // dlog("☁️SessionController+NIM dealRecvMessages: \(messages)") - - var createAILoadingModelFlag = false - - // 🚩处理 - for message in messages { - if dealEveryMessage(message){ - createAILoadingModelFlag = true - } - - } // end circle messages - - - // 🚩音频自动播放数据处理 - if self.isDisplaying && (aiInfo?.isAutoPlayVoice ?? 0 == 1){ - for per in messages.reversed(){ - if let model = util.findCellModel(message: per){ - if let info = model.baseRemoteInfo, let _ = info.customAttachment?.type{ - // 有类型的消息 - }else{ - model.autoPlayAudioOnce = true - break - } - }else{ - dlog("⚠️Not found cellModel by message") - } - } - } - - - if createAILoadingModelFlag{ - util.createAnAILoadingModel() - tableView.reloadData() - } - - } - - private func dealEveryMessage(_ message: V2NIMMessage) -> Bool{ - var createAILoadingModelFlag = false - // 一些事件是在Attachment - let info = IMRemoteUtil.dealRemoteInfo(message: message) - - if message.isSelf{ - createAILoadingModelFlag = true - dlog("☁️[onReceive] 收到一条自己的消息(服务端代发)") - // State Set - textSuggestionsView.requestNewDatasNextTime = true - } - - // 带心动分数反馈 - if let heartbeatVal = info.score, heartbeatVal != 0{ - if let aiInfo = aiInfo, let currentBeatVal = aiInfo.aiUserHeartbeatRelation?.heartbeatVal { - let total = currentBeatVal + heartbeatVal - dlog("💓heartbeatVal 新val: \(total), 本次变化: \(heartbeatVal)") - - IMAIViewModel.shared.updateHeartbeatVal(total) - sessionNavigationView.configOnlyHeartbeatVal(value: total) - } - } - - // 滑动底部 - var scrollBottom = true - if message.messageType == .MESSAGE_TYPE_CUSTOM{ - if let attachment = info.customAttachment{ - dealCustomAttachment(attachment: attachment) - - if attachment.type == .IM_SEND_GIFT{ - createAILoadingModelFlag = true - } - scrollBottom = attachment.type?.needInstantScrollDisplay ?? false - } - } - - if scrollBottom { - scrollToBottom(tableView, delay: 0.35, animated: true) - } - - return createAILoadingModelFlag - } - - // MARK: - Helper methods - private func dealCustomAttachment(attachment: IMCustomAttachment){ - guard let type = attachment.type else {return} - if type == .HEARTBEAT_LEVEL_UP{ - // sessionNavigationView.upDownNoticeView.showUnlocked(string: attachment.title ?? "-") - // sessionNavigationView.playUpgradeAnimationOnce() - IMAIViewModel.shared.heartbeatLevelUpString = attachment.title - requestUserInfo(userId: aiId) - }else if type == .HEARTBEAT_LEVEL_DOWN{ - // sessionNavigationView.upDownNoticeView.showLose(string: attachment.title ?? "-") - IMAIViewModel.shared.heartbeatLevelDownString = attachment.title - requestUserInfo(userId: aiId) - }else if type == .INSUFFICIENT_BALANCE{ - IMAIViewModel.shared.showChatModelInsufficentCoinSheet() - } - } - - public func dealFirstMessage() { - if let model = self.util.cellModels.first, model.cellType == .tips { - return - } - insertFirstTipsMessage(text: "Content generated by AI"); - } - - private func insertFirstTipsMessage(text: String) { - let tipMessage = IMMessageMaker.msgWithTips(text) - util.inserMessageWith(message: tipMessage, atIndex: 0) - tableView.reloadData() - } - -} - -// MARK: - NIMChatManagerDelegate - -extension SessionController: V2NIMMessageListener { - /// 收到一条新消息(被动发送 + 收到的消息 - func onReceive(_ messages: [V2NIMMessage]) { - dlog("☁️SessionNIN V2NIMMessageListener 收到消息:\(messages.count)条") - guard let message = messages.first else{ - return - } - guard let messageConversationId = message.conversationId, messageConversationId == conversationId else{ - return - } - - - util.clearAILoadingModel() - _ = util.appendWith(messages: messages) - tableView.reloadData() - - // 处理一些业务逻辑 - dealRecvMessagesEvents(messages: messages) - - markReadAll() - } - - - - /// 本端发送消息状态回调(主动发送) - func onSend(_ message: V2NIMMessage) { - dlog("☁️本端发送消息状态回调.\(message.text ?? "-"), serverExtension: \(message.serverExtension ?? "-"), raw:\(message.attachment?.raw ?? "-")") - if let model = util.findCellModel(message: message){ // 更新 - model.v2msg = message - tableView.reloadData() - }else{ - util.appendWith(messages: [message]) - tableView.reloadData() - scrollToBottom(tableView, delay: 0, animated: true) - } - - // 发送成功✅,需要做的: - if message.sendingState == .MESSAGE_SENDING_STATE_SUCCEEDED{ - if let model = util.findCellModel(message: message){ - if let type = model.baseRemoteInfo?.customAttachment?.type{ - if type == .CALL{ - return - } - } - util.createAnAILoadingModel() - tableView.reloadData() - scrollToBottom(tableView, delay: 0, animated: true) - } - - textSuggestionsView.requestNewDatasNextTime = true - } - } - - /** - * 更新消息在线同步通知 - * 更新消息多端同步通知 - * 更新消息漫游通知 - */ - func onReceiveMessagesModified(_ messages: [V2NIMMessage]) { - dlog("☁️消息被修改.\(messages)") - for per in util.cellModels { - for updatePer in messages { - if let updateMsgId = updatePer.messageServerId, let nowMsgId = per.v2msg?.messageServerId, nowMsgId == updateMsgId{ - per.v2msg = updatePer - per.baseRemoteInfo = IMRemoteUtil.dealRemoteInfo(message: updatePer) - dlog("☁️消息更新ed,\(updatePer.text ?? "")") - break - } - } - } - tableView.reloadData() - } - - -} diff --git a/crush/Crush/Src/Modules/Chat/Session/SessionController+Operate.swift b/crush/Crush/Src/Modules/Chat/Session/SessionController+Operate.swift deleted file mode 100755 index 132cf03..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/SessionController+Operate.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// SessionController+Operate.swift -// LegendTeam -// -// Created by 梁博 on 20/12/21. -// - -import Foundation -import UIKit -import NIMSDK - -extension SessionController { - func requestUserInfo(userId: Int) { - if userId <= 0 { - return - } - - IMAIViewModel.shared.aiId = userId - - AIRoleProvider.request(.imUserBaseInfo(aiId: userId), modelType: IMAIUserInfo.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case let .success(model): - if model != nil { - self?.aiInfo = model - IMAIViewModel.shared.updateAI(model) - - self?.reloadViews() - - // 首次加载数据 - self?.loadMessages {[weak self] messageDatas, error in - if self?.firstLoadedUser == false{ - self?.scrollToBottom(self?.tableView) - self?.firstLoadedUser = true - } - } - - } - case let .failure(error): - switch error { - case let .serviceError(code, _): - if code == .userExist || code == .aiRoleNotExist { - let alert = Alert(title: "The Role has been deleted", text: "The role has been deleted and cannot be accessed and interacted.") - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) {[weak self] in - self?.close() - } - alert.addAction(action1) - alert.show() - } - default: - break - } - break - } - } - } - - func testOperate(){ - - // Create image message - let imagePath = AppConst.cachePath - - let message = V2NIMMessageCreator.createImageMessage(imagePath, name: "imageName", sceneName: "nim_defalt_im", width: 200, height: 200) - - - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/SessionController+Other.swift b/crush/Crush/Src/Modules/Chat/Session/SessionController+Other.swift deleted file mode 100644 index d7265d2..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/SessionController+Other.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// SessionController+Other.swift -// Crush -// -// Created by Leon on 2025/8/28. -// - -extension SessionController{ - func testFunc(){ - util.createAnAILoadingModel() - tableView.reloadData() - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/SessionController.swift b/crush/Crush/Src/Modules/Chat/Session/SessionController.swift deleted file mode 100755 index 23f3031..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/SessionController.swift +++ /dev/null @@ -1,463 +0,0 @@ - -import NIMSDK -import SnapKit -import TZImagePickerController -import UIKit -import Combine -class SessionController: CLBaseViewController { - var sessionNavigationView: SessionNavigationView! - var bgImageView: UIImageView! - var overlay: GradientView! - - var tableView: UITableView! - // var headView: SessionAIHeadView! - - // MARK: BottomViews - var bottomViewsStackV : UIStackView! - var inputEntrance: SessionInputOperateView! - var inputBar: SessionInputView! - var moreView: IMMoreItemView! - var giftSendView: GiftGridSendView! - var textSuggestionsView : SessionInputSuggestionsView! - - var voiceHoldView: IMVoiceHoldView! - var pureBgOperateView:SessionPureBgOperateView! - - - // 长按菜单响应的cell - var menuCell: SessionCell? - - // IM user info - var aiInfo: IMAIUserInfo? - - // -- Layout - var bottomConstraintForInputBar: Constraint? - - // -- flag - var firstLoadedUser = false; - var isDisappearing = false// 是否正在消失 - var dealCancelEditing = false// 是否正在退出键盘输入 - var kuolieReportMatchedChatOnce = false // v1.6.0 kuolie - var isRequesting = false - var isPullingToAIHomePage = false - - // --- view model - lazy var giftVM = GiftViewModel() - - /// 此版本也是aiId - var aiId: Int! - /// 439257063882753@r@t - var accountId:String! - /// eg: 439213911113729@u@t|1|439257063882753@r@t - var conversationId: String! - var conversation: V2NIMConversation! - - var util: SessionUtil! = SessionUtil() - - var cancellables = Set() - - convenience init(accountID: String) { - self.init() - accountId = accountID - - let array = accountID.components(separatedBy: "@") - if let number = Int(array.first ?? "0"){ - aiId = number - } - - self.conversationId = V2NIMConversationIdUtil.p2pConversationId(accountID) - if self.conversationId != nil{ - conversation = V2NIMConversation() - NIMSDK.shared().v2ConversationService.getConversation(conversationId) {[weak self] conversation in - self?.conversation = conversation - } failure: { error in - dlog("☁️❌get \(String(describing: self.conversationId)) conversation error:\(error)") - } - }else{ - dlog("❌p2pConversationId failed") - } - } - - convenience init(conversationId: String) { - self.init() - self.conversationId = conversationId - conversation = V2NIMConversation() - - let stings = conversationId.components(separatedBy: "|") - guard let last = stings.last else{return} - accountId = last - let strings2 = last.components(separatedBy:"@") - if let userIdStr = strings2.first { - if let userid = Int(userIdStr) { - aiId = userid - } - } - - NIMSDK.shared().v2ConversationService.getConversation(conversationId) {[weak self] conversation in - self?.conversation = conversation - } failure: { error in - dlog("☁️❌get \(String(describing: self.conversationId)) conversation error:\(error)") - } - } - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupData() - setupEvent() - - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - (navigationController as? CLNavigationController)?.disabledFullScreenPan() - IMAIViewModel.shared.updateAI(aiInfo) - IMManager.shared.addCache(sessionID: conversationId) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - isDisappearing = false - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - isDisappearing = true - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - isDisappearing = false - (navigationController as? CLNavigationController)?.enabledFullScreenPan() - cancelEditing() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - // 动态调整tableView的contentInset - adjustTableViewContentInset() - } - - deinit { - IMManager.shared.deleteCache(sessionID: conversationId) - IMManager.shared.clearUnreadCountBy(ids: [conversationId]) - } -} - -// MARK: - Views - -extension SessionController { - func setupUI() { - view.clipsToBounds = true - navigationView.backgroundColor = .clear - - sessionNavigationView = { - let v = SessionNavigationView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - } - return v - }() - - bgImageView = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview() // 得加,不然图片高度不够撑满 - } - return v - }() - // bgImageView.image = UIImage(named: "egpic")?.cropImageTop(with: 1 / UIScreen.aspectRatio) - - overlay = { - let v = GradientView(colors: [UIColor.c.cbn.withAlphaComponent(1), UIColor.c.cbn.withAlphaComponent(0), UIColor.c.cbn.withAlphaComponent(0), UIColor.c.cbn.withAlphaComponent(1)], gradientType: .topToBottom) - v.imBgOverlayMode = true - view.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - setupInputView() - - - setupTableView() - - view.bringSubviewToFront(sessionNavigationView) - view.bringSubviewToFront(bottomViewsStackV) - } - - func setupUserInfo() { - - let imUserInfo = IMUserKit.imUserKitWith(accId: accountId) {[weak self] kit in - if self?.conversation.conversationId == kit?.accountId{ - self?.navigationView.titleLabel.text = kit?.nickname - } - } - - if let userId = imUserInfo.userId, userId > 0 { - // titleView.titleLabel.text = kitInfo.showName - Hud.showIndicator() - requestUserInfo(userId: userId) - } else if aiId > 0 { - Hud.showIndicator() - requestUserInfo(userId: aiId) - }else{ - assert(false) - } - } - - func reloadViews() { - guard let user = aiInfo else { - return - } - - bgImageView.loadImage(user.backgroundImg, completionBlock: {[weak self] result in - switch result { - case let .success(imageResult): - let image = imageResult.image - self?.bgImageView.snp.makeConstraints { make in - make.width.equalTo((self?.bgImageView.snp.height)!).multipliedBy(image.size.width / image.size.height) - } - //self?.bgImageView.image = image.cropImageTop(with: 1/UIScreen.aspectRatio) - case .failure: - return - } - }) - - sessionNavigationView.config(user: user) - - tableView.reloadData() - // 刷新一些页面上的其他数据(通过UserInfo) - } -} - -// MARK: Datas & Events - -extension SessionController { - func setupData() { - setupNIM() - - util.conversationId = conversationId - - setupUserInfo() - - WalletCore.shared.refreshWallet() - // 进入页面需要标记已读 - markReadAll() - } - - - - -} - -// MARK: - Noti - -extension SessionController { - @objc func keyboardWillChanged(noti: Notification) { -// guard tableView.window != nil else { -// return -// } - if isDisappearing || isDisplaying == false { - return - } - guard let userInfo = noti.userInfo else { return } - - // 获取参数 - let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval - let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey]! as! Int - let beginFrame = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue - let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue - - var hideKeyboardFlag = false - var showKeyboardFlag = false - - // dlog("⌨️duration:\(duration)") - - if beginFrame.origin.y < UIScreen.height, endFrame.origin.y >= UIScreen.height { - // 收起键盘 - hideKeyboardFlag = true - //dlog("⌨️close keyboard") - } else if beginFrame.origin.y >= UIScreen.height, endFrame.origin.y < UIScreen.height { - // 弹出键盘 - showKeyboardFlag = true - //dlog("⌨️show keyboard") - } else if beginFrame.size.height != endFrame.size.height { - // 键盘高度变更 - //dlog("⌨️height of keyboard changed") - } - - - if hideKeyboardFlag{ // 将要关键盘 - self.doHideKeyBordActions() - }else if showKeyboardFlag{ // 将要显示键盘 - self.doKeyboardShowActions() - let offsetY = UIScreen.height - endFrame.origin.y// - UIWindow.safeAreaBottom - self.updateInputEntrance(offsetY: offsetY, duration: duration, curve: curve) - } - else if !self.dealCancelEditing { - let offsetY = UIScreen.height - endFrame.origin.y// - UIWindow.safeAreaBottom - self.updateInputEntrance(offsetY: offsetY, duration: duration, curve: curve) - } - - UIView.animate(withDuration: duration, - delay: 0, - options: UIView.AnimationOptions(rawValue: UInt(curve) << 16), - animations: { - self.view.layoutIfNeeded() - - self.scrollToBottom(self.tableView, animated: true) - }) - - } - - @objc func menuHide(noti: Notification) { - guard let menu = noti.object as? UIMenuController else { return } - guard let menuItems = menu.menuItems else { return } - guard let first = menuItems.first else { return } - if first.action == Selector(("copyAction")) { - UIMenuController.shared.menuItems = nil - } else { - } - } - - @objc func notifyChatSettingUpdated(){ - requestUserInfo(userId: aiId) - } - - @objc func notifiyRelationHiddenUpdate(){ - requestUserInfo(userId: aiId) - } - - @objc func notifiyRelationInfoUpdate(){ - requestUserInfo(userId: aiId) - } -} - -// MARK: - Action - -// BottomViews -extension SessionController { - @objc func titleTapAction() { - guard let user = aiInfo, let aiId = user.aiId else { return } - AppRouter.goAIRoleHome(aiId: aiId) - } - - - - /// 主动退出键盘 - @objc func cancelEditing() { - dealCancelEditing = true - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.dealCancelEditing = false - } - - if inputBar.textView.isFirstResponder{ - view.endEditing(true) - }else{ - hideOperateView() - } - - } - - func doKeyboardShowActions(){ - self.inputBar.isHidden = false - - self.inputEntrance.isHidden = true - // self.moreView.isHidden = true - //showMoreItems(show: false) - } - - @objc func doHideKeyBordActions() { - // showHeaderView(show: true, duration: 0.3) - - UIView.animate(withDuration: 0.1) { - self.inputBar.alpha = 0 - self.inputBar.layoutIfNeeded() - } completion: { finished in - self.inputBar.isHidden = true - self.inputBar.alpha = 1 - } - -// self.inputEntrance.isHidden = false -// self.bottomViewsStackV.setNeedsDisplay() -// self.bottomViewsStackV.layoutIfNeeded() -// showMoreItems(show: false) - hideAllBottomViews(except: [inputEntrance]) - updateInputEntrance(offsetY: 0, duration: 0, curve: UIView.AnimationCurve.easeOut.rawValue) // 0.3 - - } -} - - -// MARK: - Session Audio Phone call delegaet. - -// -// extension SessionController: SessionHeadPhoneCallGuideDelegate { -// func headPhoneCallGuideTapCall() { -// guard AudioRecordTool.audioAuth() else { -// return -// } -// -// guard AudioPlayTool.audioChannelFreeToUse(), PhoneManager.isInPhoneChannel() == false else { -// return -// } -// -// hideCallGuide() -// phoneCallHandler.onSelectVoice(user: userInfo, session: session) -// } -// } - -// MARK: - Animation - -extension SessionController { -// func showMoreItem(show: Bool) { -// if show { -// UIView.animate(withDuration: 0.3) { -// self.moreView.alpha = 1 -// self.moreView.snp.updateConstraints { make in -// make.top.equalTo(self.inputBar.snp.bottom).offset(-UIWindow.safeAreaBottom) -// } -// self.view.layoutIfNeeded() -// } completion: { _ in -// } -// } else { -// UIView.animate(withDuration: 0.3) { -// self.moreView.alpha = 0 -// self.moreView.snp.updateConstraints { make in -// make.top.equalTo(self.inputBar.snp.bottom).offset(10) -// } -// self.view.layoutIfNeeded() -// } completion: { _ in -// } -// } -// } - func updateInputEntrance(offsetY: CGFloat, duration: TimeInterval, curve: Int) { - let offsetY = offsetY > 0 ? offsetY : (UIWindow.safeAreaBottom) - - self.bottomViewsStackV.snp.updateConstraints { make in - make.bottom.equalTo(self.view).offset(-offsetY) - } -// self.bottomViewsStackV.setNeedsDisplay() -// self.bottomViewsStackV.layoutIfNeeded() -// -// // 添加这行来动态调整tableView的contentInset -// DispatchQueue.main.async { [weak self] in -// self?.adjustTableViewContentInset() -// } - } - - func updateTableContentOffset(duration: TimeInterval){ - UIView.animate(withDuration: duration) { - self.updateTableViewContentLayout() - self.view.layoutIfNeeded() - } - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/View/SessionAIHeadView.swift b/crush/Crush/Src/Modules/Chat/Session/View/SessionAIHeadView.swift deleted file mode 100644 index ee30667..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/View/SessionAIHeadView.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// SessionAIHeadView.swift -// Crush -// -// Created by Leon on 2025/8/20. -// -import UIKit - -class SessionAIHeadView: UITableViewCell{ - - var block: UIView! - var effectView : UIVisualEffectView! - var introductionLabel: LineSpaceLabel! - var tagsStackH : UIStackView! - var expandButton: EPIconGhostButton! - - var heightChangeAction: ((_ height:CGFloat)->Void)? - - var data: IMAIUserInfo? - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.backgroundColor = .clear - v.cornerRadius = 16 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 8, left: 24, bottom: 8, right: 24)) -// make.leading.equalToSuperview().offset(24) -// make.trailing.equalToSuperview().offset(-24) -// make.top.equalToSuperview().offset(8) - } - return v - }() - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 0.9 - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - introductionLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbm) - v.config(typo) - v.numberOfLines = 3 - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.bottom.equalToSuperview().offset(-48) - } - return v - }() - - expandButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .xs, iconCode: .iconFullimage) - v.addTarget(self, action: #selector(tapExpandButton), for: .touchUpInside) - block.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.bottom.equalToSuperview().offset(-8) - make.size.equalTo(CGSize(width: 40, height: 40)) - } - return v - }() - - tagsStackH = { - let v = UIStackView() - v.spacing = 8 - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalTo(introductionLabel.snp.bottom).offset(8) - } - return v - }() - } - - private func setupData(){ - - } - - private func testData(){ - introductionLabel.text = "Intro: She is a new trainee teacher who has just graduated. You are the most rebellious student in the whole school. In order to prove her ability, she took the initiative to apply to be your class teacher. In the upcoming college entrance examination, you two…" - - do { - let tag = RoleTag() - tag.title = "Sensibility" - tag.style = .purple - tagsStackH.addArrangedSubview(tag) - } - - do { - let tag = RoleTag() - tag.title = "Romantic" - tag.style = .theme - tagsStackH.addArrangedSubview(tag) - } - } - - private func setupEvent(){ - - } - - func refresh(_ user: IMAIUserInfo?){ - self.data = user - - guard let info = user else {return} - - introductionLabel.text = info.introduction - - tagsStackH.removeSubviews() - if let character = info.characterName{ - let tag = RoleTag() - tag.title = character - tag.style = .blurPurple - tagsStackH.addArrangedSubview(tag) - } - - if let tagName = info.tagName{ - let tag = RoleTag() - tag.title = tagName - tag.style = .blurTheme - tagsStackH.addArrangedSubview(tag) - } - } - - // MARK: Action - - @objc private func tapExpandButton(){ - guard let introduction = data?.introduction else{return} - - let vc = RoleIntroBrowseDialogController() - vc.introduction = introduction - vc.imageUrl = data?.backgroundImg - vc.show() - } - - override func layoutSubviews() { - super.layoutSubviews() - let maxY = block.frame.maxY + 8 - heightChangeAction?(maxY) - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/View/SessionCell.swift b/crush/Crush/Src/Modules/Chat/Session/View/SessionCell.swift deleted file mode 100755 index 8f962c8..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/View/SessionCell.swift +++ /dev/null @@ -1,352 +0,0 @@ -// -// SessionCell.swift -// LegendTeam -// -// Created by 梁博 on 17/12/21. -// - -import UIKit -import Lottie -import NIMSDK -protocol SessionCellDelegate: NSObjectProtocol{ - func longPressAction(model: SessionBaseModel?, cell: SessionCell?) - func retryAction(model: SessionBaseModel?) - func onTapCellAction(event: IMEventModel?) -} - -class SessionCell: UITableViewCell { - var containerView: IMContentBaseView! - var bubbleImageView: UIImageView! - private var loadingView: LottieAnimationView? - var retryButton: UIButton! - - weak var delegate: SessionCellDelegate? - var model: SessionBaseModel? - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - self.selectionStyle = .none - self.backgroundColor = .clear - - let press = UILongPressGestureRecognizer(target: self, action: #selector(longPress(reco:))) - addGestureRecognizer(press) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - // 不规则corner -// if self.containerView.bounds.size.height > 0 { -// guard let cellModel = self.model else { -// return -// } -// if cellModel.shouldShowCardView || cellModel.shouldShowTips { -// // 不需要被裁减layer的情况 -// self.containerView.layer.mask = nil -// return -// } -// // 添加圆角 -// var corner:UIRectCorner! -// if cellModel.shouldShowLeft { -// corner = [.topLeft, .topRight, .bottomRight] -// } else { -// corner = [.topLeft, .topRight, .bottomLeft] -// } -// -// let maskPath = UIBezierPath(roundedRect: self.containerView.bounds, byRoundingCorners: corner, cornerRadii: CGSize(width: 20, height: 20)) -// let maskLayer = CAShapeLayer() -// maskLayer.masksToBounds = true -// maskLayer.frame = self.containerView.bounds -// maskLayer.path = maskPath.cgPath -// self.containerView.layer.mask = maskLayer -// } - } - - override func prepareForReuse() { - super.prepareForReuse() - endLodingAnimation() - } - - func setupUI() { - guard let model = model else { return } - - let type = model.config.contentViewClass(model: model) - containerView = type.init() - contentView.addSubview(containerView) - containerView.delegate = self - containerView.snp.makeConstraints { make in - make.center.equalToSuperview() - } - - retryButton = UIButton(type: .custom) - contentView.addSubview(retryButton) - retryButton.isHidden = true - retryButton.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - retryButton.setImage(UIImage(named: "icon_im_notice"), for: .normal) - retryButton.addTarget(self, action: #selector(retryAction), for: .touchUpInside) - retryButton.snp.makeConstraints { make in - make.center.equalToSuperview() - } - - bubbleImageView = UIImageView() - bubbleImageView.contentMode = .scaleToFill - containerView.addSubview(bubbleImageView) - containerView.sendSubviewToBack(bubbleImageView) - bubbleImageView.isHidden = true - bubbleImageView.snp.makeConstraints { make in - make.leading.equalTo(self.containerView.snp.leading) - make.trailing.equalTo(self.containerView.snp.trailing) - make.top.equalTo(self.containerView.snp.top) - make.bottom.equalTo(self.containerView.snp.bottom) - } - } - - private func reloadViews(model: SessionBaseModel) { - if containerView == nil { - setupUI() - } - - let onlyShowContent = model.config.onlyShowContent(model: model) - let insets = model.config.cellInsets(model: model) - let size = model.config.contentSize(model: model) - let contentInets = model.config.contentInsets(model: model) - // 内容view 大小 - let contentSize = CGSize(width: size.width + contentInets.left + contentInets.right, height: size.height + contentInets.top + contentInets.bottom) - -// dLog("contentSize___\(contentSize)___size___\(size)__type__\(model.cellType)") - - if onlyShowContent { - containerView.snp.remakeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(contentSize) - } - } else { - if model.shouldShowLeft { - containerView.snp.remakeConstraints { make in - make.leading.equalToSuperview().offset(insets.left) - make.top.equalToSuperview().offset(insets.top) - make.size.equalTo(contentSize) - } - - retryButton.snp.remakeConstraints { make in - make.leading.equalTo(containerView.snp.trailing).offset(8) - make.centerY.equalTo(containerView) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - - loadingView?.snp.remakeConstraints { make in - make.leading.equalTo(containerView.snp.trailing).offset(8) - make.centerY.equalTo(containerView) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - } else { - containerView.snp.remakeConstraints { make in - make.trailing.equalToSuperview().offset(-insets.right) - make.top.equalToSuperview().offset(insets.top) - make.size.equalTo(contentSize) - } - - retryButton.snp.remakeConstraints { make in - make.trailing.equalTo(containerView.snp.leading).offset(-8) - make.centerY.equalTo(containerView) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - - loadingView?.snp.remakeConstraints { make in - make.trailing.equalTo(containerView.snp.leading).offset(-8) - make.centerY.equalTo(containerView) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - } - } - - contentView.layoutIfNeeded() - } - - private func startLoadingAnimation() { - if self.loadingView == nil { - let animation = LottieAnimation.named("single_ring") - let animationView = LottieAnimationView(animation: animation) - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - addSubview(animationView) - self.loadingView = animationView - } - self.loadingView?.isHidden = false - self.loadingView?.play(completion: { _ in - }) - reloadLoadingLayout() - } - - private func endLodingAnimation() { - self.loadingView?.isHidden = true - self.loadingView?.stop() - } - - func reloadLoadingLayout() { - guard let theModel = model else { return } - if (theModel.shouldShowLeft) { - loadingView?.snp.remakeConstraints { make in - make.leading.equalTo(containerView.snp.trailing).offset(8) - make.centerY.equalTo(containerView) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - } else { - loadingView?.snp.remakeConstraints { make in - make.trailing.equalTo(containerView.snp.leading).offset(-8) - make.centerY.equalTo(containerView) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - } - } -} - -// -extension SessionCell { - public func bindCell(model: SessionBaseModel) { - self.model = model - - reloadViews(model: model) - containerView.refreshModel(model: model) - - bubbleImageView.isHidden = !model.shouldShowBubble - if model.shouldShowLeft { - bubbleImageView.backgroundColor = .clear - } else { - if let bubble = IMAIViewModel.shared.aiIMInfo?.chatBubble{// , bubble.isDefault.boolValue == false - // Kinfinser load image - ImageDownloader.downloadImage(from: bubble.webImgUrl) {[weak self] image in - guard let downloadBubble = image else{ - self?.useDefaultBubbleStyle() - return - } - // ⚠️拉伸图片不能比实际最小的高度还高 - let scaleImage = downloadBubble.scaled(toHeight: 68) - let stretchedImage = scaleImage!.stretchableImage(horizontalRatio: 0.5, verticalRatio: 0.5) - self?.bubbleImageView.image = stretchedImage - } - }else{ - useDefaultBubbleStyle() - } - - } - - // loading hidden - if activityLoadingHidden() || model.isDeriveModel || model.config.onlyShowContent(model: model) { - endLodingAnimation() - } else { - startLoadingAnimation() - } - - dealRetryButton() - } - - private func useDefaultBubbleStyle(){ - let image = UIImage(named: "chat_bubble_default")! - let scaleImage = image.scaled(toHeight: 44) - let stretchedImage = scaleImage!.stretchableImage(horizontalRatio: 0.5, verticalRatio: 0.5) - bubbleImageView.image = stretchedImage - } - - /// 消息体左边的指示 - func dealRetryButton() { - - // 初始设置为retry按钮,初始Hidden - retryButton.isHidden = disableRetryButton() - retryButton.isUserInteractionEnabled = true - let retryIcon = UIImage(named: "icon_im_retry") - retryButton.setImage(retryIcon, for: .normal) - - guard let model = self.model else { return } - guard let message = model.v2msg else { return } - - // 初始预定状态下,必定会被隐藏的 - let mustHide = model.isDeriveModel || model.shouldShowCardView - - let info = IMRemoteUtil.dealRemoteInfo(message: message) - if info.cellType == .keyword , model.shouldShowBubble { - retryButton.isHidden = false || mustHide - retryButton.isUserInteractionEnabled = false - retryButton.setImage(UIImage(named: "icon_im_notice"), for: .normal) - } - - if message.sendingState == .MESSAGE_SENDING_STATE_FAILED{ - if message.messageStatus.errorCode == 20001{ - // 没业务权限发送照片 - retryButton.isHidden = false - retryButton.setImage(UIImage(named: "icon_im_notice"), for: .normal) - } else if message.messageStatus.errorCode == 20000{ - // 余额不足 - retryButton.isHidden = false - retryButton.setImage(UIImage(named: "icon_im_notice"), for: .normal) - } - } - } - - func activityLoadingHidden() -> Bool { - guard let message = model?.v2msg else{ - return true - } - - if message.isSelf{ - return message.sendingState != .MESSAGE_SENDING_STATE_SENDING - }else{ - return true - } - -// guard let message = model?.message else { return true } -// if !message.isReceivedMsg { -// return message.deliveryState != .delivering -// } else { -// return message.attachmentDownloadState != .downloading -// } - } - - func disableRetryButton() -> Bool { - guard let cellModel = model else { return true } - guard let message = cellModel.v2msg else { return true } - - if message.messageType == .MESSAGE_TYPE_TIP { - return true - } -// if cellModel.isDeriveModel || cellModel.shouldShowCardView { -// return true -// } -// if !message.isReceivedMsg { -// return message.deliveryState != .failed -// } else if message.from == NIMSDK.shared().loginManager.currentAccount() { -// return !message.isBlackListed -// } else { -// return message.attachmentDownloadState != .failed -// } - return true - } -} - -// MARK: Action - -extension SessionCell { - @objc func retryAction() { - delegate?.retryAction(model: self.model) - } - - @objc func longPress(reco: UIGestureRecognizer) { - if reco.state == .began { - delegate?.longPressAction(model: self.model, cell:self) - } - } - -} - -extension SessionCell: IMContentViewDelegate { - func onTapAction(event: IMEventModel) { - delegate?.onTapCellAction(event: event) - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/View/SessionHeartLevelNoticeView.swift b/crush/Crush/Src/Modules/Chat/Session/View/SessionHeartLevelNoticeView.swift deleted file mode 100644 index 8497c48..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/View/SessionHeartLevelNoticeView.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// SessionHeartLevelNoticeView.swift -// Crush -// -// Created by Leon on 2025/8/25. -// - -class SessionHeartLevelNoticeView: UIView{ - var label : CLLabel! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - cornerRadius = 16 - snp.makeConstraints { make in - make.height.equalTo(32) - } - label = { - let v = CLLabel() - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - return v - }() - - } - - private func setupData(){ - alpha = 0 - transform = .identity - } - - private func setupEvent(){ - - } - - // MARK: - Public - func showUnlocked(string: String?, completion:(()->Void)? = nil){ - // 从下往上位移 + 淡入 - transform = CGAffineTransform(translationX: 0, y: 30) - alpha = 0 - UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseOut]) { - self.alpha = 1 - self.transform = .identity - } - backgroundColor = .c.cpn - label.text = string//"Unlocked \"\(string ?? "-")\" title" - - // 隐藏 - DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {[weak self] in - UIView.animate(withDuration: 0.5) { - self?.alpha = 0 - completion?() - } - } - } - - func showLose(string: String?, completion:(()->Void)? = nil){ - // 从上往下位移 + 淡入 - transform = CGAffineTransform(translationX: 0, y: -30) - alpha = 0 - UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseOut]) { - self.alpha = 1 - self.transform = .identity - } - backgroundColor = .c.cwn - label.text = string//"Losed the title of \"\(string ?? "-")\"" - - DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {[weak self] in - UIView.animate(withDuration: 0.5) { - self?.alpha = 0 - completion?() - } - } - } - -} diff --git a/crush/Crush/Src/Modules/Chat/Session/View/SessionNavigationView.swift b/crush/Crush/Src/Modules/Chat/Session/View/SessionNavigationView.swift deleted file mode 100644 index 06af6db..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/View/SessionNavigationView.swift +++ /dev/null @@ -1,414 +0,0 @@ -// -// SessionNavigationView.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import UIKit -import Lottie -import Combine -class SessionNavigationView: UIView { - lazy var navigationView: NavigationView = { - let view = NavigationView() - return view - }() - - var naviMoreButton: EPIconGhostButton! - var moreBadge: BadgeView! - var likeView : HeartLikeCountView! - - // -- One Avatar - var newPeopleContainer: UIView! - var avatar: CLImageView! - var newPeopleStackV: UIStackView! - var nameLabel: CLLabel! - var likesLabel: CLLabel! - var newPeopleAvatarTopButton: UIButton! - - // -- Two Avatars - var heartLevelContainer: UIView! - var aiAvatar: CLImageView! - var mineAvatar: CLImageView! - var hearIcon: UIImageView! - var heartLottie: LottieAnimationView! - var heartLevelLabel: UILabel! - var heartTopButton: UIButton! - var aiAvatarTopButton: UIButton! - var mineAvatarTopButton: UIButton! - - // -- 额外区域 - var heartLevelTag: RelationshipTag! - /// 升降级的NoticeView - var upDownNoticeView : SessionHeartLevelNoticeView! - - var data: IMAIUserInfo? - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - - private func setupData() { - refreshMoreButtonBadge() - } - - private func setupEvent() { - IMAIViewModel.shared.$heartbeatLevelUpString.sink {[weak self] string in - guard let notice = string, notice.count > 0 else {return} - self?.upDownNoticeView.showUnlocked(string: notice) - self?.playUpgradeAnimationOnce() - }.store(in: &cancellables) - - IMAIViewModel.shared.$heartbeatLevelDownString.sink {[weak self] string in - guard let notice = string, notice.count > 0 else {return} - self?.upDownNoticeView.showLose(string: notice) - self?.playUpgradeAnimationOnce() - }.store(in: &cancellables) - - NotificationCenter.default.addObserver(self, selector: #selector(notiMoreButtonBadgeChanged), name: AppNotificationName.chatNaviMoreRedDotChanged.notificationName, object: nil) - } - - // MARK: - Public - - func config(user: IMAIUserInfo?) { - data = user - guard let userInfo = user else { - return - } - - if userInfo.aiUserHeartbeatRelation?.heartbeatLevel != nil { - // 有心动等级 - navigationView.snp.updateConstraints { make in - make.bottom.equalToSuperview().offset(-40) - } - heartLevelContainer.isHidden = false - newPeopleContainer.isHidden = true - - aiAvatar.loadImage(userInfo.aiUserHeartbeatRelation?.aiHeadImg) - mineAvatar.loadImage(userInfo.aiUserHeartbeatRelation?.userHeadImg) - - heartLevelLabel.text = "\(userInfo.aiUserHeartbeatRelation?.heartbeatLevelNum ?? 0)" - - guard let level = userInfo.aiUserHeartbeatRelation?.heartbeatLevel, let heartBeatVal = userInfo.aiUserHeartbeatRelation?.heartbeatVal else { - return - } - heartLevelTag.isHidden = false - heartLevelTag.bind(level: level, heartBeatVal: heartBeatVal, isShow: userInfo.aiUserHeartbeatRelation?.isShow) - - } else { - // 初始等级 - navigationView.snp.updateConstraints { make in - make.bottom.equalToSuperview() - } - heartLevelContainer.isHidden = true - newPeopleContainer.isHidden = false - - avatar.loadImage(userInfo.headImg) - nameLabel.text = userInfo.nickname - - let number = NSNumber(value: userInfo.likedNum ?? 0) -// #warning("test") -// let number = NSNumber(value: 19320) - - let displayNumber = String.displayNumber(number, scale: 1) - likesLabel.text = "\(displayNumber) likes" - } - - likeView.isLike = userInfo.liked ?? false - } - - func configOnlyHeartbeatVal(value: CGFloat?){ - guard value != nil else{return} - - guard let userInfo = data else { - return - } - - // let heartbeatVal = userInfo.aiUserHeartbeatRelation?.heartbeatVal - guard let level = userInfo.aiUserHeartbeatRelation?.heartbeatLevel else { - return - } - heartLevelTag.isHidden = false - heartLevelTag.bind(level: level, heartBeatVal: value, isShow: userInfo.aiUserHeartbeatRelation?.isShow) - - } - - func playUpgradeAnimationOnce(){ - heartLottie.isHidden = false - self.heartLottie.play {[weak self] completed in - self?.heartLottie.isHidden = true - } - } - - // MARK: - Helper - - func refreshMoreButtonBadge(){ - let config = AppCache.fetchCache(key: CacheKey.chatRedBadgeConfig.rawValue, type: ChatSettingCacheConfig.self) ?? ChatSettingCacheConfig() - moreBadge.isHidden = config.chatBackgroundTapped && config.chatBubbleTapped - } - - @objc private func notiMoreButtonBadgeChanged(){ - refreshMoreButtonBadge() - } - - // MARK: - Actions, button点击在外部 - - // MARK: - UI - func addNavigationView() { - addSubview(navigationView) - - navigationView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.height.equalTo(UIWindow.navBarTotalHeight) - make.bottom.equalToSuperview().offset(-40) - } - } - - private func setupViews() { - addNavigationView() - navigationView.bgView.alpha = 0 - - navigationView.paddingRightForRightStack = 2 - likeView = { - let v = HeartLikeCountView(viewSize: .xl) - v.purIconStyle() - navigationView.rightStackH.addArrangedSubview(v) - return v - }() - - naviMoreButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .chatroomMore) - navigationView.rightStackH.addArrangedSubview(v) - - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 52, height: 44)) - } - return v - }() - - moreBadge = { - let v = BadgeView() - v.onlyShowPoint = true - navigationView.addSubview(v) - v.snp.makeConstraints { make in -// make.top.equalTo(naviMoreButton) -// make.trailing.equalTo(naviMoreButton) - make.centerX.equalTo(naviMoreButton).offset(10) - make.centerY.equalTo(naviMoreButton).offset(-6) - } - v.badgeValue = 1 - return v - }() - - setupBasicContainer() - setupHeartLevel() - - heartLevelTag = { - let v = RelationshipTag() - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom).offset(4) - } - v.isHidden = true - return v - }() - - upDownNoticeView = { - let v = SessionHeartLevelNoticeView() - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(heartLevelContainer.snp.bottom).offset(4) - make.centerX.equalToSuperview() - } - return v - }() - } - - private func setupBasicContainer() { - newPeopleContainer = { - let v = UIView() - navigationView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(navigationView.titleLabel) - make.height.equalTo(44) - make.leading.equalTo(navigationView.leftStackH.snp.trailing) - make.trailing.equalTo(navigationView.rightStackH.snp.leading) - } - return v - }() - - avatar = { - let v = CLImageView(frame: .zero) - let wh = 40.0 - v.layer.cornerRadius = wh * 0.5 - v.layer.masksToBounds = true - newPeopleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: wh, height: wh)) - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - newPeopleAvatarTopButton = { - let v = UIButton() - newPeopleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - newPeopleStackV = { - let v = UIStackView() - v.axis = .vertical - v.alignment = .leading - newPeopleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(avatar.snp.trailing).offset(8) - make.trailing.lessThanOrEqualToSuperview() - } - return v - }() - - nameLabel = { - let v = CLLabel() - v.font = .t.ttm - newPeopleStackV.addArrangedSubview(v) - return v - }() - - likesLabel = { - let v = CLLabel() - v.font = .t.tls - newPeopleStackV.addArrangedSubview(v) - return v - }() - } - - private func setupHeartLevel() { - heartLevelContainer = { - let v = UIView() - navigationView.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalTo(navigationView.titleLabel) - make.height.equalTo(44) - } - return v - }() - - heartLevelContainer.isHidden = true - - aiAvatar = { - let v = CLImageView(frame: .zero) - let wh = 40.0 - v.layer.cornerRadius = wh * 0.5 - v.layer.masksToBounds = true - heartLevelContainer.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: wh, height: wh)) - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - mineAvatar = { - let v = CLImageView(frame: .zero) - let wh = 40.0 - v.layer.cornerRadius = wh * 0.5 - v.layer.masksToBounds = true - heartLevelContainer.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: wh, height: wh)) - make.trailing.equalToSuperview() - make.leading.equalTo(aiAvatar.snp.trailing).offset(24) - make.centerY.equalToSuperview() - } - return v - }() - - hearIcon = { - let v = UIImageView() - v.image = UIImage(named: "chat_level_heart") - heartLevelContainer.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 44, height: 44)) - make.center.equalToSuperview() - } - return v - }() - - heartLottie = { - let animation = LottieAnimation.named("heartbeat_pink") - let animationView = LottieAnimationView(animation: animation) - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .playOnce - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - animationView.size = CGSize(width: 44, height: 44) - heartLevelContainer.addSubview(animationView) - animationView.snp.makeConstraints { make in - make.center.equalTo(hearIcon) - make.size.equalTo(CGSize(width: 240, height: 210)) - } - animationView.isHidden = true - return animationView - }() - - heartLevelLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tnmm - heartLevelContainer.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalTo(hearIcon) - } - return v - }() - - aiAvatarTopButton = { - let v = UIButton() - heartLevelContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalTo(aiAvatar) - } - return v - }() - - mineAvatarTopButton = { - let v = UIButton() - heartLevelContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalTo(mineAvatar) - } - return v - }() - - heartTopButton = { - let v = UIButton() - heartLevelContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalTo(hearIcon) - } - return v - }() - } - -} diff --git a/crush/Crush/Src/Modules/Chat/Session/View/SessionPureBgOperateView.swift b/crush/Crush/Src/Modules/Chat/Session/View/SessionPureBgOperateView.swift deleted file mode 100644 index d8b3c81..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/View/SessionPureBgOperateView.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// SessionPureBgOperateView.swift -// Crush -// -// Created by Leon on 2025/9/22. -// - -class SessionPureBgOperateView: UIView{ - var block: UIView! - var backButton: EPIconTertiaryDarkButton! - var shareButton: EPIconTertiaryDarkButton! - var middleButton: StyleButton! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.height.equalTo(80) - } - return v - }() - - backButton = { - - let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .large, iconCode: .arrowLeftBorder) - let size = v.bgImageSize() - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.centerY.equalToSuperview() - make.size.equalTo(size) - } - return v - }() - - shareButton = { - let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .large, iconCode: .shareBorder) - let size = v.bgImageSize() - block.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.centerY.equalToSuperview() - make.size.equalTo(size) - } - return v - }() - - middleButton = { - let v = StyleButton() - v.contrastTertiaryDark(size: .large) - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(backButton.snp.trailing).offset(16) - make.trailing.equalTo(shareButton.snp.leading).offset(-16) - } - return v - }() - middleButton.setTitle("Chat Background", for: .normal) - } -} diff --git a/crush/Crush/Src/Modules/Chat/Session/ViewModel/IMAIViewModel.swift b/crush/Crush/Src/Modules/Chat/Session/ViewModel/IMAIViewModel.swift deleted file mode 100644 index 409f1fb..0000000 --- a/crush/Crush/Src/Modules/Chat/Session/ViewModel/IMAIViewModel.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// IMAIViewModel.swift -// Crush -// -// Created by Leon on 2025/8/26. -// -import Combine -/// 单例 -class IMAIViewModel { - static var shared = IMAIViewModel() - - - /// Current Chat aiId - var aiId: Int = 0 - @Published var aiIMInfo: IMAIUserInfo? { - didSet { - if let id = aiIMInfo?.aiId, id > 0{ - aiId = id - } - chatBubble = aiIMInfo?.chatBubble - chatBackgroundImg = aiIMInfo?.backgroundImg - heartbeatVal = aiIMInfo?.aiUserHeartbeatRelation?.heartbeatVal - } - } - - @Published var heartbeatVal : CGFloat?{ - didSet { - aiIMInfo?.aiUserHeartbeatRelation?.heartbeatVal = heartbeatVal - } - } - - @Published var heartbeatLevelUpString: String?{ - didSet{ - if let string = heartbeatLevelUpString, string.count > 0{ - heartbeatLevelUpString = nil // reset - } - } - } - @Published var heartbeatLevelDownString: String?{ - didSet{ - if let string = heartbeatLevelDownString, string.count > 0{ - heartbeatLevelDownString = nil - } - } - } - - var chatBubble: ChatBubble? - /// 从IM Base info中或者, 从 Chat setting接口中处理。 - var chatBackgroundImg: String? - - - - var chatSetting: IMChatSetting? - - // MARK: - Public - func updateAI(_ ai: IMAIUserInfo?){ - aiIMInfo = ai - } - - func updateHeartbeatVal(_ val: CGFloat?){ - guard let theVal = val else{return} - heartbeatVal = theVal - } - - // MARK: - Functions - - func showChatModelInsufficentCoinSheet() { - guard aiId > 0 else { - return - } - - loadChatSetting { result, chatSetting in - if result, let setting = chatSetting { - let sheet = ChatModePickInsufficientCoinSheet(currentSelectedModelCode: setting.modelCode) - sheet.show() - } - } - } - - // MARK: Setting - - func loadChatSetting(completion: ((Bool, IMChatSetting?) -> Void)?) { - guard aiId > 0 else { - completion?(false, nil) - assert(false) - return - } - - AIRoleProvider.request(.chatSetupGet(aiId: aiId), modelType: IMChatSetting.self) { [weak self] result in - switch result { - case let .success(model): - self?.chatSetting = model - completion?(true, model) - case .failure: - completion?(false, nil) - } - } - } - - func updateChatModel(code: String?, completion: ((Bool) -> Void)?) { - var params = [String: Any]() - - params.updateValue(aiId, forKey: "aiId") - params.updateValue(code ?? "", forKey: "code") - - AIRoleProvider.request(.chatModelSetup(params: params), modelType: EmptyModel.self) { result in - Hud.hideIndicator() - switch result { - case .success: - // 发送通知更新其他页面 - NotificationCenter.post(name: .chatSettingUpdated) - completion?(true) - case .failure: - completion?(false) - } - } - } -} diff --git a/crush/Crush/Src/Modules/Chat/SessionList/SessionListController.swift b/crush/Crush/Src/Modules/Chat/SessionList/SessionListController.swift deleted file mode 100644 index 873b74f..0000000 --- a/crush/Crush/Src/Modules/Chat/SessionList/SessionListController.swift +++ /dev/null @@ -1,332 +0,0 @@ -// -// SessionListController.swift -// Crush -// -// Created by Leon on 2025/8/13. -// - -import JXSegmentedView -import UIKit -class SessionListController: CLViewController { - - var sessions = [V2NIMConversation]() - var lastedResult: V2NIMConversationResult? - - var msgStat: MessageStat? - - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - setupViews() - setupDatas() - setupEvents() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if sessions.count == 0{ - - } - loadMessageStat() - } - - private func setupViews() { - navigationView.isHidden = true - view.backgroundColor = .clear - - addRefreshHeader() - addRefreshFooter() - container.delegate = self - } - - func addRefreshHeader() { - RefreshHeaderAnimator { [weak self] in - self?.loadMessageStat() - self?.loadData() - }.link(to: container.tableView) - } - - func addRefreshFooter() { - RefreshFooterAnimator { [weak self] in - self?.loadData(more: true) - }.link(to: container.tableView) - } - - private func setupDatas() { - setupSessions() - } - - private func setupSessions() { - container.tableView.mj_footer?.endRefreshing() - sessions.removeAll() - refreshTable() - - if sessions.count == 0 { - loadData() - } - } - - private func setupEvents() { - NIMSDK.shared().v2ConversationService.add(self) - - NotificationCenter.default.addObserver(self, selector: #selector(notifiyRelationHiddenUpdate), name: AppNotificationName.heartbeatRelationHiddenUpdate.notificationName, object: nil) - } - - func loadData(more:Bool = false) { - guard NIMSDK.shared().v2LoginService.getLoginStatus() == .LOGIN_STATUS_LOGINED else { - container.tableView.mj_footer?.endRefreshing() - return - } - var offset:Int64 = 0 - if(more){ - offset = lastedResult?.offset ?? 0 - } - - - NIMSDK.shared().v2ConversationService.getConversationList(offset, limit: 20) { [weak self] result in - self?.container.tableView.mj_header?.endRefreshing() - - self?.lastedResult = result - - if let conversations = result.conversationList, conversations.count > 0 { - - if more{ - self?.sessions.append(contentsOf: conversations) - }else{ - self?.sessions = conversations - self?.container.tableView.mj_footer?.resetNoMoreData() - // dlog("☁️conversations:\(conversations)") - } - self?.refreshTable() - - var accountIds = [String]() - for per in conversations { - if let accountId = V2NIMConversationIdUtil.conversationTargetId(per.conversationId){ - accountIds.append(accountId) - } - } - self?.loadUserInfos(ids: accountIds) - } - - if result.finished{ - self?.container.tableView.mj_footer?.endRefreshingWithNoMoreData() - }else{ - self?.container.tableView.mj_footer?.endRefreshing() - } - } failure: { error in - dlog("☁️load conversations error: \(error.description)") - } - } - - func loadMessageStat(){ - IMManager.shared.regetNoticeUnread() - } - - func loadUserInfos(ids: [String]){ - guard ids.count > 0 else {return} - - NIMSDK.shared().v2UserService.getUserList(ids) { users in - dlog("☁️getUserList✅") - } - } - - func deleteAllConversations(){ - var conversations = [String]() - - var aiIds = [Int]() - for per in sessions { - let conversationId = per.conversationId - if !conversations.contains(where: { str in - str == conversationId - }){ - conversations.append(conversationId) - if let aiId = IMHelperUtil.getAIIdByConversationId(conversationId){ - aiIds.append(aiId) - } - } - } - - if conversations.count > 0{ - NIMSDK.shared().v2ConversationService.deleteConversationList(byIds: conversations, clearMessage: true) { resultList in - dlog("☁️删除所有会话结果,成功结果:\(resultList)") - - if resultList.count > 0{ - aiIds.removeAll() - for per in resultList { - let conversationId = per.conversationId - if let aiId = IMHelperUtil.getAIIdByConversationId(conversationId){ - aiIds.append(aiId) - } - } - } - - if aiIds.count > 0{ - let params = ["aiIdList":aiIds] - AICowProvider.request(.aiMessageDel(params: params), modelType: EmptyModel.self) { result in - switch result { - case .success: - break - case .failure: - break - } - } - } - }failure: { error in - dlog("❌💬 deleteConversationList error: \(error)") - } - - - } - } - - // MARK: - Helper - - func refreshTable() { - - sortSessions() - - container.cellModels = sessions - container.tableView.reloadData() - if sessions.count > 0 { - container.removeEmpty() - } else { - container.showEmpty(text: "No Data~") - } - } - - func sortSessions() { - sessions.sort { recent1, recent2 in - let time1: Double = recent1.lastMessage?.messageRefer.createTime ?? 0 - let time2: Double = recent2.lastMessage?.messageRefer.createTime ?? 0 - return time1 > time2 - } - } -} - -extension SessionListController{ - @objc private func notifiyRelationHiddenUpdate(){ - loadData() - } -} - -// MARK: - SessionListViewDelegate -extension SessionListController: SessionListViewDelegate{ - func selectRow(indexPath: IndexPath) { - if (indexPath.section == 0){ - AppRouter.goNoticeCenter() - return - } - - let conversation = sessions[indexPath.row] - let conversationId = conversation.conversationId - AppRouter.goChatVC(conversationId: conversationId, complete: nil) - -// #warning("test") -// let sessionId = "439217670979585@r@t" -// AppRouter.goChatVC(accId: sessionId, complete: nil) - } - - // Delete single conversation - func deleteRow(indexPath: IndexPath) { - let conversation = sessions[indexPath.row] - let conversationId = conversation.conversationId - - //dlog("conversationId: \(conversationId)") - let aiId = IMHelperUtil.getAIIdByConversationId(conversationId) - //dlog("aiId : \(String(describing: aiId))") - NIMSDK.shared().v2ConversationService.deleteConversation(conversationId, clearMessage: true) { - dlog("☁️delete conversation ok✅") - guard let theAIId = aiId else{return} - let params = ["aiIdList":[theAIId]] - AICowProvider.request(.aiMessageDel(params: params), modelType: EmptyModel.self) { result in - switch result { - case .success: - break - case .failure: - break - } - } - } failure: { error in - dlog("☁️delete conversation failed, error:\(error.description)") - } - - } -} - -// MARK: NIMConversationManagerDelegate - -extension SessionListController: V2NIMConversationListener { - func onSyncStarted() { - //dlog("☁️[Conversation]onSyncStarted") - } - - func onSyncFinished() { - //dlog("☁️[Conversation]onSyncFinished") - // let totalUnreadCount = NIMSDK.shared().v2ConversationService.getTotalUnreadCount() - setupSessions() - refreshTable() - } - - func onSyncFailed(_ error: V2NIMError) { - dlog("☁️[Conversation]onSyncFailed:\(error)") - } - - func onConversationCreated(_ conversation: V2NIMConversation) { - sessions.insert(conversation, at: 0) - refreshTable() - } - - /// 会话变更回调 - func onConversationChanged(_ conversations: [V2NIMConversation]) { - dlog("☁️[Conversation]Changed") - // 1. 把更新的会话转成字典,方便快速查找 - let updateMap = Dictionary(uniqueKeysWithValues: conversations.map { ($0.conversationId, $0) }) - - // 2. 遍历 sessions,如果有对应更新就替换 - for (index, session) in sessions.enumerated() { - if let updated = updateMap[session.conversationId] { - sessions[index] = updated - } - } - refreshTable() - - // conversation中可能有新消息,重新获取未读数量 - IMManager.shared.regetConversationAllUnreadCount() - } - - func onConversationDeleted(_ conversationIds: [String]) { - sessions.removeAll { per in - for perId in conversationIds { - if per.conversationId == perId { - return true - } - } - return false - } - refreshTable() - } - -// func onTotalUnreadCountChanged(_ unreadCount: Int) { -// // See more in IMManager -// } - - // 账号多端登录会话已读时间戳标记通知回调 - func onConversationReadTimeUpdated(_ conversationId: String, readTime: TimeInterval) { - } -} - -// MARK: - Noti - -extension SessionListController { - @objc func logout() { - } -} - -extension SessionListController: JXSegmentedListContainerViewListDelegate { - func listView() -> UIView { - return view - } -} diff --git a/crush/Crush/Src/Modules/Chat/SessionList/View/SessionListCell.swift b/crush/Crush/Src/Modules/Chat/SessionList/View/SessionListCell.swift deleted file mode 100644 index 6618276..0000000 --- a/crush/Crush/Src/Modules/Chat/SessionList/View/SessionListCell.swift +++ /dev/null @@ -1,284 +0,0 @@ -// -// SessionListCell.swift -// Crush -// -// Created by Leon on 2025/8/13. -// - -import UIKit -import SwipeCellKit -class SessionListCell: SwipeTableViewCell { - - var avatarView: CLImageView! - var badge: BadgeView! - - var contentStackV : UIStackView! - var namesStackH : UIStackView! - var nameLabel: UILabel! - var cardSwipeIcon: UIImageView! - var relationTag: RelationshipTag! - var contentLabel: UILabel! // May be ActiveLabel - - var rightItemsV:UIStackView! - var timeLabel: UILabel! - var heartbeatView: HeartBeatView! - - - // -- Data - var data : V2NIMConversation! - - 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 - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews(){ - avatarView = { - let v = CLImageView() - contentView.addSubview(v) - let wh = 48.0; - v.layer.cornerRadius = wh * 0.5 - v.layer.masksToBounds = true - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: wh, height: wh)) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - badge = { - let v = BadgeView() - v.showMax99 = true - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(avatarView) - make.trailing.equalTo(avatarView).offset(4) - } - return v - }() - - rightItemsV = { - let v = UIStackView() - v.axis = .vertical - v.alignment = .trailing - v.spacing = 6 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.centerY.equalToSuperview() - } - return v - }() - - timeLabel = { - let v = UILabel() - v.font = .t.tbs - v.textColor = .c.ctsn - v.setContentCompressionResistancePriority(UILayoutPriority(755), for: .horizontal) - rightItemsV.addArrangedSubview(v) - return v - }() - - heartbeatView = { - let v = HeartBeatView(viewSize: .small) - rightItemsV.addArrangedSubview(v) - return v - }() - - contentStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 4 - v.alignment = .leading - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(avatarView.snp.trailing).offset(12) - make.trailing.lessThanOrEqualTo(rightItemsV.snp.leading).offset(-12) - make.centerY.equalToSuperview() - } - return v - }() - - namesStackH = { - let v = UIStackView() - v.spacing = 4 - v.alignment = .center - contentStackV.addArrangedSubview(v) - return v - }() - - nameLabel = { - let v = UILabel() - v.font = .t.tts - v.textColor = .text - v.setContentCompressionResistancePriority(UILayoutPriority(244), for: .horizontal) - namesStackH.addArrangedSubview(v) - return v - }() - - cardSwipeIcon = { - let v = UIImageView() - v.image = UIImage(named: "card_swipe_flag") - namesStackH.addArrangedSubview(v) - v.isHidden = true - return v - }() - - relationTag = { - let v = RelationshipTag() - namesStackH.addArrangedSubview(v) - return v - }() - - contentLabel = { - let v = UILabel() - v.font = .t.tbs - v.textColor = .text - contentStackV.addArrangedSubview(v) - return v - }() - - -// #warning("test") -// testData() - } - - private func testData(){ - avatarView.loadImage(UserCore.shared.user?.headImage) - badge.badgeValue = 99 - nameLabel.text = "BBB CCC" - relationTag.type = .friend - contentLabel.text = "This is words" - heartbeatView.content = "37.8℃" - timeLabel.text = "22:28" - } - - func config(_ conversation: V2NIMConversation){ - data = conversation - - if let serverExtension = conversation.serverExtension, let info = CodableHelper.decode(IMBaseRemoteInfo.self, from: serverExtension){ - print("💬conversationId:\(conversation.conversationId), extension:\(serverExtension)") - dealServerExtension(info: info) - }else{ - dealServerExtension(info: nil) - } - - avatarView.loadImage(conversation.avatar) - nameLabel.text = conversation.name - badge.badgeValue = conversation.unreadCount - if let lastMsgCreateTime = conversation.lastMessage?.messageRefer.createTime{ - timeLabel.text = Date.timerStyle(style: .IMCHAT, second: Int(lastMsgCreateTime)) - }else { - let updateTime = conversation.updateTime - timeLabel.text = Date.timerStyle(style: .IMCHAT, second: Int(updateTime)) - } - - - contentLabel.text = contentWith(conversation: conversation) - } - - /// 配置会话数据,支持搜索高亮 - /// - Parameters: - /// - conversation: 会话数据 - /// - searchText: 搜索关键词,用于高亮显示 - func config(_ conversation: V2NIMConversation, searchText: String?) { - data = conversation - - if let serverExtension = conversation.serverExtension, let info = CodableHelper.decode(IMBaseRemoteInfo.self, from: serverExtension){ - dlog("💬conversationId:\(conversation.conversationId), extension:\(serverExtension)") - dealServerExtension(info: info) - }else{ - dealServerExtension(info: nil) - } - - avatarView.loadImage(conversation.avatar) - - // 设置名称,支持高亮显示 - if let searchText = searchText, !searchText.isEmpty, let name = conversation.name { - nameLabel.attributedText = TextHighlightHelper.highlightText(name, searchText: searchText) - } else { - nameLabel.text = conversation.name - } - - badge.badgeValue = conversation.unreadCount - if let lastMsgCreateTime = conversation.lastMessage?.messageRefer.createTime{ - timeLabel.text = Date.timerStyle(style: .IMCHAT, second: Int(lastMsgCreateTime)) - }else { - let updateTime = conversation.updateTime - timeLabel.text = Date.timerStyle(style: .IMCHAT, second: Int(updateTime)) - } - - contentLabel.text = contentWith(conversation: conversation) - } - - func dealServerExtension(info: IMBaseRemoteInfo?){ - let isShow = info?.isShow.trueOrNil - - heartbeatView.isHidden = false - if let heartbeatVal = info?.heartbeatVal{ - heartbeatView.content = "\(heartbeatVal)" - }else{ - heartbeatView.content = "" - } - - if let level = info?.heartbeatLevel, isShow == true{ - relationTag.bind(level: level) - relationTag.isHidden = false - }else{ - relationTag.isHidden = true - } - } - - func contentWith(conversation: V2NIMConversation) -> String { - guard let message = conversation.lastMessage else { return "No messages" } - if message.messageType == .MESSAGE_TYPE_CUSTOM { - return dealCustom(message: message) - } - if String.realEmpty(str: message.text) && message.serverExtension == nil && message.messageType == .MESSAGE_TYPE_TEXT { - return dealCustom(message: message) - } - return dealNormal(message: message) - } -} - -// MARK: - Helper -extension SessionListCell{ - func dealCustom(message: V2NIMLastMessage) -> String { -// let remoteInfo = IMRemoteUtil.dealRemoteInfo(message: <#T##V2NIMMessage?#>) -// return "[Message]" - if let raw = message.attachment?.raw, let attchment = CodableHelper.decode(IMCustomAttachment.self, from: raw){ - return attchment.getDisplayString() - } - return "" - } - - func dealNormal(message: V2NIMLastMessage) -> String { - var content = "" - - if String.realEmpty(str: content){ - content = message.text ?? "" - } - return content - } -} diff --git a/crush/Crush/Src/Modules/Chat/SessionList/View/SessionListView.swift b/crush/Crush/Src/Modules/Chat/SessionList/View/SessionListView.swift deleted file mode 100644 index 9788833..0000000 --- a/crush/Crush/Src/Modules/Chat/SessionList/View/SessionListView.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// SessionListView.swift -// Crush -// -// Created by Leon on 2025/8/13. -// - -import UIKit -import SwipeCellKit -import Combine - -protocol SessionListViewDelegate: NSObjectProtocol { - func selectRow(indexPath: IndexPath) - /// 删除单条消息 - func deleteRow(indexPath: IndexPath) -} - -class SessionListView: UIView{ - var tableView: UITableView! - weak var delegate : SessionListViewDelegate? - - // -- Data - var cellModels = [V2NIMConversation]() - var msgStat: MessageStat? - - private var cancellables = Set() - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - tableView = { - let v = UITableView(frame: .zero, style: .plain) - v.separatorStyle = .none - v.delegate = self - v.dataSource = self - v.estimatedRowHeight = 72 - v.backgroundColor = .clear - v.contentInsetAdjustmentBehavior = .never - v.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: UIWindow.safeAreaBottom + 16, right: 0) - if #available(iOS 15.0, *) { - v.sectionHeaderTopPadding = 0 - } - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - - tableView.register(SessionListCell.self, forCellReuseIdentifier: "SessionListCell") - tableView.register(SessionNoticeCell.self, forCellReuseIdentifier: "SessionNoticeCell") - - } - - - - private func setupData(){ - - } - - private func setupEvent(){ - IMManager.shared.$noticeStat.sink {[weak self] stat in - self?.config(stat) - }.store(in: &cancellables) - } - - private func config(_ stat: MessageStat?){ - msgStat = stat - tableView.reloadData() - } -} - -extension SessionListView: UITableViewDelegate, UITableViewDataSource{ - - func numberOfSections(in tableView: UITableView) -> Int { - return 2 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0{ - return 1 - } - return cellModels.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section == 0{ - let cell = tableView.dequeueReusableCell(withIdentifier: "SessionNoticeCell", for: indexPath) as! SessionNoticeCell - cell.config(msgStat) - return cell - } - let cell = tableView.dequeueReusableCell(withIdentifier: "SessionListCell", for: indexPath) as! SessionListCell - let conversation = cellModels[indexPath.row] - cell.config(conversation) - cell.delegate = self - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { -// let vc = SessionController() -// viewController()?.navigationController?.pushViewController(vc, animated: true) - delegate?.selectRow(indexPath: indexPath) - } - - -} - - -extension SessionListView: SwipeTableViewCellDelegate { - func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeCellKit.SwipeActionsOrientation) -> [SwipeCellKit.SwipeAction]? { - guard orientation == .right else { return nil } - - let deleteAction = SwipeAction(style: .default, title: nil) { [weak self] cellAction, _ in - - #warning("to do, 点击取消或者删除,cell没有恢复原状") - - let alert = Alert(title: "Tips", text: "Are you sure to delete the chat") - let action1 = AlertAction(title: "Delete", actionStyle: .destructive) { - self?.delegate?.deleteRow(indexPath: indexPath) - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - - cellAction.fulfill(with: .reset) -// dlog("删除了") -// self?.delegate?.deleteRow(indexPath: indexPath) - } - - deleteAction.backgroundColor = .c.cin - deleteAction.title = "Delete" - return [deleteAction] - } - - func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { - var options = SwipeOptions() - options.expansionStyle = .fill - // options.transitionStyle = .border - return options - } - -} diff --git a/crush/Crush/Src/Modules/Chat/SessionList/View/SessionNoticeCell.swift b/crush/Crush/Src/Modules/Chat/SessionList/View/SessionNoticeCell.swift deleted file mode 100644 index 53e2cfb..0000000 --- a/crush/Crush/Src/Modules/Chat/SessionList/View/SessionNoticeCell.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// SessionNoticeCell.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import UIKit - -class SessionNoticeCell: UITableViewCell { - var iconBlock: UIView! - var icon: UIImageView! - var badge: BadgeView! - - var contentStackV: UIStackView! - var titleLabel: UILabel! - var contentLabel: UILabel! - - var timeLabel: UILabel! - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - iconBlock = { - let v = UIView() - v.backgroundColor = .c.cpn - v.layer.cornerRadius = 24 - v.layer.masksToBounds = true - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.leading.equalToSuperview().offset(24) - make.bottom.equalToSuperview().offset(-16) - make.size.equalTo(CGSize(width: 48, height: 48)) - } - return v - }() - - icon = { - let v = UIImageView() - iconBlock.addSubview(v) - v.image = MWIconFont.image(fromIcon: .messages, size: CGSize(width: 20, height: 20), color: .white) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - badge = { - let v = BadgeView() - v.showMax99 = true - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(iconBlock) - make.trailing.equalTo(iconBlock).offset(4) - } - return v - }() - - timeLabel = { - let v = UILabel() - v.font = .t.tbs - v.textColor = .c.ctsn - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(iconBlock) - } - return v - }() - - contentStackV = { - let v = UIStackView() - v.axis = .vertical - v.alignment = .leading - v.spacing = 4 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(iconBlock.snp.trailing).offset(12) - make.trailing.lessThanOrEqualTo(timeLabel.snp.leading).offset(-12) - make.centerY.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.tts - v.textColor = .text - contentStackV.addArrangedSubview(v) - return v - }() - - contentLabel = { - let v = UILabel() - v.font = .t.tbs - v.textColor = .white - contentStackV.addArrangedSubview(v) - return v - }() - - titleLabel.text = "Notice" - contentLabel.text = " " - timeLabel.text = " " - - //testData() - } - - private func testData() { - timeLabel.text = "22:28" - titleLabel.text = "Notice" - badge.badgeValue = 100 - contentLabel.text = "Welcome to BGMDOG to learn about Crush Level" - } - - func config(_ stat: MessageStat?){ - guard let data = stat else {return} - - badge.badgeValue = data.unRead ?? 0 - contentLabel.text = data.latestContent - if let timestamp = data.latestTime{ - timeLabel.text = Date.timerStyle(style: .IMCHAT, millisecond: timestamp) - }else{ - timeLabel.text = "" - } - } -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/ChatBackgroundGridController.swift b/crush/Crush/Src/Modules/Chat/Setting/ChatBackgroundGridController.swift deleted file mode 100644 index a920af6..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/ChatBackgroundGridController.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// ChatBackgroundGridController.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit - -class ChatBackgroundGridController: CLViewController { - var aiId : Int? - var selectBgUrl: String? - - var backgrounds: [IMChatBackground]? - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - - private func setupViews() { - navigationView.alpha0Title = "Chat Background" - navigationView.setupBackButtonCloseIcon() - } - - private func setupDatas(){ - container.aiId = aiId - loadBgList() - - guard let aiInfo = IMAIViewModel.shared.aiIMInfo else{return} - - let level = aiInfo.aiUserHeartbeatRelation?.heartbeatLevel ?? .level1 - container.headView.setupCreateMode(create: level.isGreaterOrEqual(to: .level10)) - - if selectBgUrl == nil{ - loadChatSetting() - } - - } - - private func loadChatSetting(){ - guard let theId = aiId else{return} - Hud.showIndicator() - AIRoleProvider.request(.chatSetupGet(aiId: theId), modelType: IMChatSetting.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - if let select = model?.backgroundImg{ - self?.selectBgUrl = select - self?.container.config(self?.backgrounds, selectBgUrl: self?.selectBgUrl) - } - case .failure: - break - } - } - } - - private func loadBgList(){ - guard let theAiId = aiId else{ - return - } - Hud.showIndicator() - AIRoleProvider.request(.getChatBackgroundList(aiId: theAiId), modelType: [IMChatBackground].self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - self?.backgrounds = model - self?.container.config(model, selectBgUrl: self?.selectBgUrl) - case .failure: - break - } - } - } - - private func setupEvents(){ - container.bottomButton.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - container.headView.operateButton.addTarget(self, action: #selector(tapUnlockOrCreateNewBg), for: .touchUpInside) - - NotificationCenter.default.addObserver(self, selector: #selector(notifyBackgroundListUpated), name: AppNotificationName.chatSettingBackgroundListUpdated.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyBackgroundUpdated), name: AppNotificationName.chatSettingBackgroundChanged.notificationName, object: nil) - } - - // MARK: - Action - @objc private func tapBottomButton(){ - guard let theAiId = aiId else{ - return - } - - var params = [String:Any]() - params.updateValue(theAiId, forKey: "aiId") - - if let selectModel = container.selectBgModel, let id = selectModel.backgroundId{ - params.updateValue(id, forKey: "backgroundId") - } - - Hud.showIndicator() - AIRoleProvider.request(.setDefaultChatBackground(params: params), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - NotificationCenter.post(name: .chatSettingBackgroundChanged) - NotificationCenter.post(name: .chatSettingUpdated) - self?.close(dismissFirst: true) - case .failure: - break - } - } - - } - - @objc private func tapUnlockOrCreateNewBg(){ - guard let aiInfo = IMAIViewModel.shared.aiIMInfo else{return} - - if let level = aiInfo.aiUserHeartbeatRelation?.heartbeatLevel, level.isGreaterOrEqual(to: .level10) { - let vc = ChatBackgroundGenerateController() - vc.aiId = aiId - presentNaviRootVc(vc: vc) - }else{ - AppRouter.goAIHeartBeatLevelPage(aiId: aiInfo.aiId) - } - } - - - - // MARK: - Noti - - @objc private func notifyBackgroundListUpated(){ - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {[weak self] in - self?.loadBgList() - } - } - - @objc private func notifyBackgroundUpdated(){ - - close(dismissFirst: true) - } -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/ChatBubbleGridController.swift b/crush/Crush/Src/Modules/Chat/Setting/ChatBubbleGridController.swift deleted file mode 100644 index e0a7732..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/ChatBubbleGridController.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// ChatBubbleGridController.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit - -class ChatBubbleGridController: CLViewController { - var aiId : Int? - var selectChatBubbleCode: String? - - var data: IMChatSetting? - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - - private func setupViews() { - navigationView.setupBackButtonCloseIcon() - navigationView.alpha0Title = "Chat Bubble" - } - - private func setupDats(){ - loadBubbles() - } - - private func loadBubbles(){ - guard let theAiId = aiId else{ - return - } - Hud.showIndicator() - CommonProvider.request(.chatBubbleDict(aiId: theAiId), modelType: [IMChatBubble].self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - self?.container.config(model, selectCode: self?.selectChatBubbleCode) - case .failure: - break - } - } - } - - private func setupEvents(){ - container.bottomButton.addTarget(self, action: #selector(tapOperateButton), for: .touchUpInside) - } - - @objc private func tapOperateButton(){ - guard let theAiId = aiId, let code = container.selectCode, code.count > 0, let bubble = container.selectBubble else{ - return - } - - if bubble.unlockType == .heartbeatLevel{ - guard let level = IMAIViewModel.shared.aiIMInfo?.aiUserHeartbeatRelation?.heartbeatLevel, level.isGreaterOrEqual(to: bubble.unlockHeartbeatLevel ?? .level1) else{ - let vc = HeartBeatLevelGridController() - vc.aiId = theAiId - navigationController?.pushViewController(vc, animated: true) - return - } - } - - if bubble.unlockType == .member && UserCore.shared.user?.isMember == false{ - AppRouter.goVIPCenter() - return - } - - Hud.showIndicator() - var params = [String:Any]() - params.updateValue(theAiId, forKey: "aiId") - params.updateValue(code, forKey: "code") - AIRoleProvider.request(.chatBubbleSetup(params: params), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - NotificationCenter.post(name: .chatSettingBackgroundChanged) - NotificationCenter.post(name: .chatSettingUpdated) - self?.close(dismissFirst: true) - case .failure: - break - } - } - } -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/ChatPersonalSettingController.swift b/crush/Crush/Src/Modules/Chat/Setting/ChatPersonalSettingController.swift deleted file mode 100644 index 341f7eb..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/ChatPersonalSettingController.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// ChatPersonalSettingController.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit -import Combine -class ChatPersonalSettingController: CLViewController { - lazy var birthdayPicker: BirthdayPickerView = BirthdayPickerView() - - var setting: IMChatSetting? - - @Published var buttonEnable:Bool = false - private var cancellables = Set() - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - navigationView.alpha0Title = "My Chat Persona" - navigationView.setupNaviRightStyleButton() - navigationView.styleMainButton.setTitle("Save", for: .normal) - navigationView.styleMainButton.addTarget(self, action: #selector(tapNaviSaveButton), for: .touchUpInside) - } - - private func setupDatas() { - } - - private func setupEvents() { - container.birthdaySelectView.selectBlock = { [weak self] in - self?.selectBirthday() - } - - navigationView.tapBackButtonAction = { [weak self] in - if self?.container.editFlag ?? false{ - self?.alertToExitNotSaved() - }else{ - self?.close() - } - } - - $buttonEnable.sink {[weak self] enable in - self?.navigationView.styleMainButton.isEnabled = enable - }.store(in: &cancellables) - - Publishers.CombineLatest4(container.$whoIm.prepend(nil), container.$birthdayDate.prepend(nil), container.$nickname.prepend(nil), container.$editFlag.prepend(false)).map{ [weak self] - whoIm, date, name, editFlag in - guard let birthdayDate = date, let nickname = name else{ - return false - } - - if Date().years(from: birthdayDate) < 18{ - return false - } - - if !nickname.isValidNickname { - return false - } - - if let content = whoIm{ - if content.trimmed.count > 0 && content.count < 10{ - return false - } - } - - // Add: 没有更改.判断方法1, 直接判断container.editFlag -// if let oldNickname = self?.setting?.nickname, let oldBirthday = self?.setting?.birthday, let oldWhoIm = self?.setting?.whoAmI{ -// if oldNickname == name && birthdayDate.timeStamp == oldBirthday && whoIm == oldWhoIm{ -// return false -// } -// } - guard editFlag else{ - return false - } - - return true - }.assign(to: &$buttonEnable) - - container.config(setting) - } - - private func selectBirthday() { - if let date = container.birthdayDate{ - birthdayPicker.setupDefaultSelectDate(date) - }else if let dateStr = container.birthdayStr{ - let date = Date(dateString: dateStr, format: "yyyy-MM-dd") - birthdayPicker.setupDefaultSelectDate(date) - } - - birthdayPicker.show() - birthdayPicker.tapConfirmAction = { [weak self] date, timestamp in - dlog("birthday date:\(date). timestamp:\(timestamp)") - self?.container.birthdayDate = date - self?.container.birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - self?.container.editFlag = true - } - - - } - - // MARK: - Action - @objc private func tapNaviSaveButton(){ - guard let newNickname = container.nickname, let before = container.nickname else{ - return - } - - Hud.showIndicator() - if newNickname != before { - UserOperator.checkname(name: newNickname) {[weak self] nicknameOK in - if nicknameOK{ - self?.doSaveChatSetting() - }else{ - Hud.hideIndicator() - } - } - }else{ - doSaveChatSetting() - } - - } - - // MARK: - Functions - - private func doSaveChatSetting(){ - guard let aiId = setting?.aiId else {return} - var params = [String:Any]() - - let nickname = container.nickname - let birthday = container.birthdayDate!.timeStamp - let whoAmI = container.whoIm - - params.updateValue(aiId, forKey: "aiId") - params.updateValue(nickname ?? "-", forKey: "nickname") - params.updateValue(birthday, forKey: "birthday") - params.updateValue(whoAmI ?? "", forKey: "whoAmI") - - - AIRoleProvider.request(.chatSetup(params: params), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - Hud.toast(str: "保存成功") - NotificationCenter.post(name: .chatPersonaUpdated) - self?.navigationController?.popViewController(animated: true) - case .failure: - break - } - } - } - - @objc private func alertToExitNotSaved() { - view.endEditing(true) - let alert = Alert(title: "Save edited content", text: "Edits will not be saved after exiting. Are you sure you want to continue?") - let action1 = AlertAction(title: "Give up", actionStyle: .confirm) { [weak self] in - self?.close() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/ChatSettingListController.swift b/crush/Crush/Src/Modules/Chat/Setting/ChatSettingListController.swift deleted file mode 100644 index 44ae7b0..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/ChatSettingListController.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// ChatSettingListController.swift -// Crush -// -// Created by Leon on 2025/8/16. -// - -import UIKit - -class ChatSettingListController: CLViewController { - var aiId : Int! - - var data: IMChatSetting? - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - - private func setupViews() { - navigationView.alpha0Title = "Setting" - } - - private func setupDats(){ - loadMyChatSetting() - } - - private func setupEvents(){ - NotificationCenter.default.addObserver(self, selector: #selector(notifyChatSettingUpdated), name: AppNotificationName.chatSettingUpdated.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyChatSettingUpdated), name: AppNotificationName.chatPersonaUpdated.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyChatBackgroundChanged), name: AppNotificationName.chatSettingBackgroundChanged.notificationName, object: nil) - } - - private func loadMyChatSetting(){ - Hud.showIndicator() - AIRoleProvider.request(.chatSetupGet(aiId: aiId), modelType: IMChatSetting.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - self?.data = model - self?.container.config(model) - case .failure: - break - } - } - } - - @objc private func notifyChatSettingUpdated(){ - loadMyChatSetting() - } - - @objc private func notifyChatBackgroundChanged(){ - self.navigationController?.popViewController(animated: false) - } -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/Model/ChatSettingCacheConfig.swift b/crush/Crush/Src/Modules/Chat/Setting/Model/ChatSettingCacheConfig.swift deleted file mode 100644 index 107b3ee..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/Model/ChatSettingCacheConfig.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// ChatSettingCacheConfig.swift -// Crush -// -// Created by Leon on 2025/8/25. -// - -struct ChatSettingCacheConfig: Codable{ - var chatBubbleTapped: Bool = false - var chatBackgroundTapped : Bool = false -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/View/ChatBackgroundGridView.swift b/crush/Crush/Src/Modules/Chat/Setting/View/ChatBackgroundGridView.swift deleted file mode 100644 index a1476a3..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/View/ChatBackgroundGridView.swift +++ /dev/null @@ -1,452 +0,0 @@ -// -// ChatBackgroundGridView.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit -import Combine - -class ChatBackgroundGridView: CLContainer { - var bottomButton: StyleButton! - var cv: UICollectionView! - var layout = UICollectionViewFlowLayout() - //var titleView: TitleView! - var headView: ChatBackgroundGridHeadView! - var headHeight: CGFloat = 168 - - var aiId : Int? - var inUseUrl: String? - var datas: [IMChatBackground] = [IMChatBackground]() - @Published var selectBgUrl: String? - var selectBgModel: IMChatBackground? - - private var cancellables = Set() - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bottomButton = { - let v = StyleButton() - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - v.isEnabled = false - return v - }() - - cv = { - let width = floor((UIScreen.width - 24 * 2 - 16) * 0.5) - let height = floor(width * 219.0 / 164.0) - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 12, left: 24, bottom: UIWindow.safeAreaBottom, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(ChatBackgroundGridCell.self, forCellWithReuseIdentifier: "ChatBackgroundGridCell") - cv.register(UICollectionReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: "UICollectionReusableView") - addSubview(cv) - cv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return cv - }() - - headView = { - let v = ChatBackgroundGridHeadView() - return v - }() - bottomButton.setTitle("Set background", for: .normal) - } - - private func setupData() { - } - - private func setupEvent() { - $selectBgUrl.sink {[weak self] str in - self?.cv.reloadData() - self?.refreshButtonState(selectImageUrl: str) - }.store(in: &cancellables) - } - - func config(_ datas: [IMChatBackground]?, selectBgUrl: String?){ - self.datas = datas ?? [] - inUseUrl = selectBgUrl - self.selectBgUrl = selectBgUrl - - if let models = datas, let selectUrl = selectBgUrl{ - for per in models { - if selectUrl == per.imgUrl!{ - selectBgModel = per - } - } - } - - cv.reloadData() - } - - // MARK: - Helper - - func refreshButtonState(selectImageUrl: String?){ - - guard let inUse = inUseUrl, let select = selectImageUrl else{ - bottomButton.isEnabled = false - return - } - - bottomButton.isEnabled = inUse != select -// if let str = selectImageUrl, str.count > 0, datas.count > 0{ // 已选中的,按钮置灰 -// bottomButton.isEnabled = true -// }else{ -// bottomButton.isEnabled = false -// } - } -} - -extension ChatBackgroundGridView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ChatBackgroundGridCell", for: indexPath) as! ChatBackgroundGridCell - let data = datas[indexPath.item] - cell.config(data) - //cell.configInUse(inUseUrl: inUseUrl) - cell.configSelectUrl(selectBgUrl) - - cell.tapFullBrowseAction = {[weak self] data, image in - guard let self = self else{return} - guard let url = data.imgUrl, !url.isEmpty else { return } - - var photoModels = [PhotoBrowserModel]() - - var startIndex = 0 - for (index, per) in self.datas.enumerated() { - let model = PhotoBrowserModel() - model.image = image - model.sourceRect = cell.screenRect ?? .zero - model.imageUrl = per.imgUrl - model.aiId = self.aiId - model.chatBackground = per - if let imageUrl = per.imgUrl, imageUrl == url{ - startIndex = index - } - photoModels.append(model) - } - - ImageBrowser.show(models: photoModels, index: startIndex, type: .chatBackgroundSet) - } - - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let data = datas[indexPath.item] - selectBgUrl = data.imgUrl - selectBgModel = data - } - - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { - let header = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: "UICollectionReusableView", - for: indexPath - ) - if headView.superview == nil || headView.superview != header { - headView.removeFromSuperview() - } - - header.addSubview(headView) - headView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - // make.bottom.equalToSuperview() - } - - return header - } - return UICollectionReusableView() - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - return CGSize(width: UIScreen.width, height: headHeight) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, alphaViews: [navigationView?.titleLabel]) - } -} - -class ChatBackgroundGridHeadView : UIView{ - var titleView: TitleView! - var creatorEntrance : UIView! - - var operateButton: StyleButton! - var stackV: UIStackView! - var titleLabel : UILabel! - var subTitleLabel:UILabel! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - titleView = { - let v = TitleView() - v.title = "Chat Background" - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.top.equalToSuperview() - } - return v - }() - - creatorEntrance = { - let v = UIView() - v.backgroundColor = .c.csen - v.cornerRadius = 16 - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(titleView.snp.bottom).offset(12) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.height.equalTo(80) - make.bottom.equalToSuperview() - } - return v - }() - - operateButton = { - let v = StyleButton() - v.primary(size: .small) - creatorEntrance.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - stackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 4 - v.alignment = .leading - creatorEntrance.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.lessThanOrEqualTo(operateButton.snp.leading).offset(-8) - make.centerY.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.tts - v.textColor = .text - stackV.addArrangedSubview(v) - return v - }() - - subTitleLabel = { - let v = UILabel() - v.font = .t.tbs - v.textColor = .c.ctsn - stackV.addArrangedSubview(v) - return v - }() - - titleLabel.text = "Create image" - - //setupCreateMode(create: true) - } - - private func setupData(){ - // Default - setupCreateMode(create: false) - } - - private func setupEvent(){ - - } - - // MARK: - Public - func setupCreateMode(create:Bool){ - if create{ - subTitleLabel.text = "Unlocked" - operateButton.setTitle("Create", for: .normal) - }else{ - subTitleLabel.text = "Heartbeat Lv.10 unlock" - operateButton.setTitle("Unlock", for: .normal) - } - } -} - -class ChatBackgroundGridCell: UICollectionViewCell { - var block: UIView! - var imageView: UIImageView! - var defaultTag:EPTagLabel! - var viewFullButon : EPIconTertiaryDarkButton! - var inUseIcon : UIImageView! - - var tapFullBrowseAction: ((_ data: IMChatBackground, _ image: UIImage?) -> Void)? - - var data: IMChatBackground? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.cornerRadius = 16 - v.backgroundColor = .c.csbn - v.layer.borderColor = UIColor.c.cpn.cgColor - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - imageView = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - viewFullButon = { - let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .small, iconCode: .iconFullimage) - v.addTarget(self, action: #selector(tapViewFullButton), for: .touchUpInside) - block.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-12) - make.bottom.equalToSuperview().offset(-12) - } - return v - }() - - defaultTag = { - let v = EPTagLabel(style: .blackOnColor) - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(8) - make.top.equalToSuperview().offset(8) - } - v.isHidden = true - return v - }() - - inUseIcon = { - let v = UIImageView() - v.image = UIImage(named: "checkmark_tick") - block.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 20, height: 20)) - make.top.equalToSuperview().offset(10) - make.trailing.equalToSuperview().offset(-8) - } - v.isHidden = true - return v - }() - - defaultTag.text = "Default" - } - - private func setupData() { - } - - private func setupEvent() { - } - - // MARK: - Public - func config(_ data: IMChatBackground?){ - self.data = data - guard let theData = data else{return} - - defaultTag.isHidden = !theData.isDefault.boolValue - imageView.loadImage(theData.imgUrl) - - self .configInused(selected: data?.isSelected ?? false) - } - - func configInUse(inUseUrl:String?){ -// guard let theDataCode = data?.imgUrl, let theUsedCode = inUseUrl, theUsedCode == theDataCode else{ -// inUseIcon.isHidden = true -// return -// } -// inUseIcon.isHidden = false - } - - func configInused(selected: Bool){ - inUseIcon.isHidden = !selected - } - - func configSelectUrl(_ url: String?){ - if let theUrl = url, let dataUrl = data?.imgUrl, theUrl == dataUrl { - setupSelected(selected: true) - }else{ - setupSelected(selected: false) - } - } - - // MARK: - Helper - - private func setupSelected(selected: Bool) { - block.layer.borderWidth = selected ? 2 : 0 - } - - // MARK: - Action - - @objc private func tapViewFullButton(){ - guard imageView.image != nil else{ - return - } - tapFullBrowseAction?(data!, imageView.image) - } -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/View/ChatBubbleGridView.swift b/crush/Crush/Src/Modules/Chat/Setting/View/ChatBubbleGridView.swift deleted file mode 100644 index f98a0ff..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/View/ChatBubbleGridView.swift +++ /dev/null @@ -1,411 +0,0 @@ -// -// ChatBubbleGridView.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit -import Combine - -class ChatBubbleGridView: CLContainer { - var bottomButton: StyleButton! - var cv: UICollectionView! - var layout = UICollectionViewFlowLayout() - var titleView: TitleView! - var headHeight: CGFloat = 60 - - var datas = [IMChatBubble]() - - var inUseCode: String? - @Published var selectCode : String? - - @Published var selectBubble: IMChatBubble? - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bottomButton = { - let v = StyleButton() - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - - cv = { - let width = floor((UIScreen.width - 24 * 2 - 16) * 0.5) - let height = 148.0 - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 12, left: 24, bottom: UIWindow.safeAreaBottom, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(ChatBubbleGridCell.self, forCellWithReuseIdentifier: "ChatBubbleGridCell") - cv.register(UICollectionReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: "UICollectionReusableView") - addSubview(cv) - cv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return cv - }() - - titleView = { - let v = TitleView() - v.title = "Chat Bubble" - return v - }() - titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: titleView.preCalculateHeight()) - bottomButton.setTitle("Use", for: .normal) - } - - private func setupData() { - } - - private func setupEvent() { - $selectCode.sink {[weak self] str in - self?.cv.reloadData() - self?.bottomButton.isEnabled = !str.isNilOrEmpty - - if str == self?.inUseCode ?? ""{ // 已选中的,按钮置灰 - self?.bottomButton.isEnabled = false - } - - for per in self?.datas ?? []{ - if per.code ?? "" == str{ - self?.selectBubble = per - break - } - } - }.store(in: &cancellables) - - $selectBubble.sink {[weak self] bubble in - guard let theBubble = bubble else{return} - switch theBubble.unlockType{ - case .none: - self?.bottomButton.primary(size: .large) - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: "Use", textFont: .t.tll,textColor: .white) - self?.bottomButton.setAttributedTitle(aStr, for: .normal) - case .member: - if UserCore.shared.user?.isMember ?? false{ - self?.bottomButton.primary(size: .large) - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: "Use", textFont: .t.tll,textColor: .white) - self?.bottomButton.setAttributedTitle(aStr, for: .normal) - }else{ - self?.bottomButton.vip(size: .large) - let aStr = StyleButton.getVIPBlackTitleByWords(words: "Member unlock") - self?.bottomButton.setAttributedTitle(aStr, for: .normal) - } - case .heartbeatLevel: - if let level = IMAIViewModel.shared.aiIMInfo?.aiUserHeartbeatRelation?.heartbeatLevel, level.isGreaterOrEqual(to: theBubble.unlockHeartbeatLevel ?? .level1){ - // 等级够了 - self?.bottomButton.primary(size: .large) - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: "Use", textFont: .t.tll,textColor: .white) - self?.bottomButton.setAttributedTitle(aStr, for: .normal) - }else{ - self?.bottomButton.primary(size: .large) - let words = theBubble.unlockHeartbeatLevel?.localizedText ?? "-" - let aStr = NSAttributedString.getIconTitleAttributeByWords(words: "Heartbeat \(words) unlock", iconImage: UIImage(named: "icon_pink_heart"), textFont: .t.tll,textColor: .white) - self?.bottomButton.setAttributedTitle(aStr, for: .normal) - } - } - }.store(in: &cancellables) - } - - func config(_ datas: [IMChatBubble]?, selectCode: String?){ - self.datas = datas ?? [] - inUseCode = selectCode - self.selectCode = selectCode - cv.reloadData() - } -} - -extension ChatBubbleGridView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ChatBubbleGridCell", for: indexPath) as! ChatBubbleGridCell - let data = datas[indexPath.row] - cell.config(data) - cell.configInUse(inUseCode: inUseCode) - cell.configSelectCode(selectCode) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let data = datas[indexPath.row] -// if data.canUseTheBubble(){ - selectCode = data.code -// } - } - - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { - let header = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: "UICollectionReusableView", - for: indexPath - ) - if titleView.superview == nil || titleView.superview != header { - titleView.removeFromSuperview() - } - - header.addSubview(titleView) - titleView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - // make.bottom.equalToSuperview() - } - - return header - } - return UICollectionReusableView() - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - return CGSize(width: UIScreen.width, height: headHeight) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, alphaViews: [navigationView?.titleLabel]) - } -} - -class ChatBubbleGridCell: UICollectionViewCell { - var block: UIView! - - var imageView: UIImageView! - var wordsLabel: CLLabel! - var defaultFlag: EPTagLabel! - - var inUseIcon : UIImageView! - var lockIcon: EPIconTertiaryDarkButton! - - var nameStackH : UIStackView! - var nameLeadingIcon : UIImageView! - var nameLabel: ColorLabel! - - var data: IMChatBubble? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.cornerRadius = 16 - v.backgroundColor = .c.csen - v.layer.borderColor = UIColor.c.cpn.cgColor - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.top.equalToSuperview() - make.height.equalTo(120) - } - return v - }() - - imageView = { - let v = UIImageView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - make.height.equalTo(44) - } - return v - }() - - defaultFlag = { - let v = EPTagLabel(style: .blackOnColor) - v.text = "Default" - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.leading.equalToSuperview().offset(8) - } - v.isHidden = true - return v - }() - - wordsLabel = { - let v = CLLabel() - v.font = .t.tbm - imageView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(imageView).offset(12) - make.bottom.equalToSuperview().offset(-12) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - - inUseIcon = { - let v = UIImageView() - v.image = UIImage(named: "checkmark_tick") - block.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 20, height: 20)) - make.top.equalToSuperview().offset(10) - make.trailing.equalToSuperview().offset(-8) - } - v.isHidden = true - return v - }() - - lockIcon = { - let v = EPIconTertiaryDarkButton(radius: .rectangle, iconSize: .small, iconCode: .iconPrivateBorder) - v.isUserInteractionEnabled = false - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - - nameStackH = { - let v = UIStackView() - v.spacing = 4 - v.alignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(block.snp.bottom).offset(8) - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - } - return v - }() - - nameLeadingIcon = { - let v = UIImageView() - nameStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 12, height: 12)) - } - v.isHidden = true - return v - }() - - nameLabel = { - let v = ColorLabel() - v.font = .t.tlm - v.textColor = .white - v.adjustsFontSizeToFitWidth = true - v.minimumScaleFactor = 0.5 - contentView.addSubview(v) - nameStackH.addArrangedSubview(v) - return v - }() - - // setupSelected(selected: true) - // nameLabel.text = "Default" - wordsLabel.text = "Hi" - } - - private func setupData() { - } - - private func setupEvent() { - } - - // MARK: - Public - func configInUse(inUseCode:String?){ - guard let theDataCode = data?.code, let theUsedCode = inUseCode, theUsedCode == theDataCode else{ - inUseIcon.isHidden = true - return - } - inUseIcon.isHidden = false - } - - func config(_ data: IMChatBubble?){ - self.data = data - - guard let theData = data else{return} - - nameLabel.text = theData.name - nameLeadingIcon.isHidden = true - if let type = theData.unlockType{ - switch type { - case .member: - nameLeadingIcon.isHidden = false - nameLeadingIcon.image = UIImage(named: "vip_flag_16") - nameLabel.applyGradient(.vip) - case .heartbeatLevel: - nameLeadingIcon.isHidden = false - nameLeadingIcon.image = UIImage(named: "heart_little_Liked_12") - nameLabel.clearGradient() - } - }else{ - nameLabel.clearGradient() - } - - - lockIcon.isHidden = theData.isUnlock.boolValue || theData.isDefault.boolValue - - defaultFlag.isHidden = !theData.isDefault.boolValue - - let imgUrl = theData.webImgUrl -// let imgUrl = "https://hhb.crushlevel.ai/static/chatBubble/chat_bubble_1.png" - imageView.loadImage(imgUrl, completionBlock:{[weak self] result in - switch result { - case let .success(imageResult): - let image = imageResult.image - let scaleImage = image.scaled(toHeight: 44) - let stretchedImage = scaleImage!.stretchableImage(horizontalRatio: 0.5, verticalRatio: 0.5) - self?.imageView.image = stretchedImage - case .failure: - return - } - }) - } - - func configSelectCode(_ code: String?){ - if let theCode = code, let dataCode = data?.code, theCode == dataCode { - setupSelected(selected: true) - }else{ - setupSelected(selected: false) - } - } - - // MARK: - Helper - - private func setupSelected(selected: Bool) { - block.layer.borderWidth = selected ? 2 : 0 - } - -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/View/ChatPersonalSettingView.swift b/crush/Crush/Src/Modules/Chat/Setting/View/ChatPersonalSettingView.swift deleted file mode 100644 index 54e83c1..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/View/ChatPersonalSettingView.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// ChatPersonalSettingView.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit -import Combine -class ChatPersonalSetting: UIView{ - var container: LTScrollContainer! - var titleView: TitleView! - var nicknameTitleField: TitleTextField! - var sexView : CLSelectView! - var birthdaySelectView: CLSelectView! - var whoImAmTextView: TitleTextView! - - @Published var nickname: String? - @Published var birthdayStr: String? - @Published var birthdayDate: Date? - @Published var whoIm: String? - - @Published var editFlag = false - - private var cancellables = Set() - - var data: IMChatSetting? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - //make.bottom.equalTo(bottomView.snp.top) - make.bottom.equalToSuperview() - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - nicknameTitleField = { - let v = TitleTextField() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - sexView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - birthdaySelectView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - whoImAmTextView = { - let v = TitleTextView() - v.titleAppendOptionalLabel() - v.maxLimit = 300 - v.minLimit = 10 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - titleView.title = "My Chat Persona" - - sexView.titleLabel.text = "Sex" - sexView.showForceNormalSupportMsg("*Gender cannot be changed") - sexView.isEnabled = false - - nicknameTitleField.titleLabel.text = "nickname".localized() - nicknameTitleField.placeholder = "Nickname" - - birthdaySelectView.titleLabel.text = "Birthday" - birthdaySelectView.placeholder = "Birthday" - - whoImAmTextView.errorMsg = "只能包含10-300字符" - whoImAmTextView.titleLabel.text = "Who I am" - whoImAmTextView.placeholder = "Describe the character characteristics and scene settings of your role…" - } - - private func setupData(){ - - } - - private func setupEvent(){ - $nickname.sink { [weak self] str in - self?.nicknameTitleField.textfield.text = str - }.store(in: &cancellables) - - $birthdayDate.sink {[weak self] date in - guard let validDate = date else{return} - let years = Date().years(from:validDate) - if years < 18{ - self?.birthdaySelectView.showErrorMsg("Must be at least 18 years old.") - }else{ - self?.birthdaySelectView.hideErrorInfo() - } - let formated = validDate.toString(dateFormat: "dd MMM, yyyy") - self?.birthdaySelectView.contentStr = formated - }.store(in: &cancellables) - - $whoIm.sink {[weak self] str in - self?.whoImAmTextView.defaultText = str - }.store(in: &cancellables) - - nicknameTitleField.textfield.textPublisher.sink {[weak self] str in - self?.nickname = str?.trimmed - self?.editFlag = true - }.store(in: &cancellables) - - whoImAmTextView.textView.textPublisher.sink { [weak self] str in - self?.whoIm = str?.trimmed - self?.editFlag = true - }.store(in: &cancellables) - } - - func config(_ data: IMChatSetting?){ - self.data = data - guard let setting = data else{return} - - sexView.contentStr = (setting.sex ?? .noncomfirming).localizedText - - nickname = setting.nickname ?? "" - if let birthday = setting.birthday{ - let date = Date.dateFromMilliseconds(birthday) - birthdayDate = date - } - - whoIm = setting.whoAmI - } -} diff --git a/crush/Crush/Src/Modules/Chat/Setting/View/ChatSettingListView.swift b/crush/Crush/Src/Modules/Chat/Setting/View/ChatSettingListView.swift deleted file mode 100644 index 5765b8c..0000000 --- a/crush/Crush/Src/Modules/Chat/Setting/View/ChatSettingListView.swift +++ /dev/null @@ -1,568 +0,0 @@ -// -// ChatSettingListView.swift -// Crush -// -// Created by Leon on 2025/8/16. -// - -import UIKit - -enum ChatSettingSection1 { - case nickname - case gender - case age - case whoIm -} - -class ChatSettingListView: CLContainer { - var tableView: UITableView! - var titleView: TitleView! - - var config: ChatSettingCacheConfig! - - var data: IMChatSetting? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - tableView = UITableView(frame: .zero, style: .plain) - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.estimatedRowHeight = 72 - tableView.backgroundColor = .clear - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIWindow.safeAreaBottom + 16, right: 0) - tableView.contentInsetAdjustmentBehavior = .never - if #available(iOS 15.0, *) { - tableView.sectionHeaderTopPadding = 0 - } - tableView.register(ChatSettingChatPersonalCell.self, forCellReuseIdentifier: "ChatSettingChatPersonalCell") - tableView.register(ChatSettingOtherCell.self, forCellReuseIdentifier: "ChatSettingOtherCell") - addSubview(tableView) - tableView.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - } - - titleView = { - let v = TitleView() - v.title = "Setting" - return v - }() - - titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: titleView.preCalculateHeight()) - tableView.tableHeaderView = titleView - } - - private func setupData() { - config = AppCache.fetchCache(key: CacheKey.chatRedBadgeConfig.rawValue, type: ChatSettingCacheConfig.self) ?? ChatSettingCacheConfig() - } - - private func setupEvent() { - } - - - // MARK: - Public - - func config(_ data: IMChatSetting?){ - self.data = data - tableView.reloadData() - } -} - -extension ChatSettingListView: UITableViewDelegate, UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return 2 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section == 0 { - let cell = tableView.dequeueReusableCell(withIdentifier: "ChatSettingChatPersonalCell", for: indexPath) as! ChatSettingChatPersonalCell - cell.config(data) - cell.tapChatPersonAction = { [weak self] in - let vc = ChatPersonalSettingController() - vc.setting = self?.data - self?.viewController()?.navigationController?.pushViewController(vc, animated: true) - } - return cell - } - - let cell = tableView.dequeueReusableCell(withIdentifier: "ChatSettingOtherCell", for: indexPath) as! ChatSettingOtherCell - cell.config(data) - cell.chatModelItem.tapTopButtonAction = {[weak self] in - self?.tapSelectChatModel() - } - - config = AppCache.fetchCache(key: CacheKey.chatRedBadgeConfig.rawValue, type: ChatSettingCacheConfig.self) ?? ChatSettingCacheConfig() - cell.chatBubble.badge.isHidden = config.chatBubbleTapped - cell.chatBackground.badge.isHidden = config.chatBackgroundTapped - - cell.chatBubble.tapTopButtonAction = { [weak self] in - guard let self = self, let aiId = data?.aiId else {return} - let vc = ChatBubbleGridController() - vc.aiId = aiId - vc.selectChatBubbleCode = data?.bubbleCode - vc.data = self.data - let navc = CLNavigationController(rootViewController: vc) - navc.modalPresentationStyle = .fullScreen - self.viewController()?.present(navc, animated: true) - - if !self.config.chatBubbleTapped{ - self.config.chatBubbleTapped = true - AppCache.cache(key: CacheKey.chatRedBadgeConfig.rawValue, value: config) - self.tableView.reloadData() - NotificationCenter.post(name: .chatNaviMoreRedDotChanged) - } - } - - cell.chatBackground.tapTopButtonAction = {[weak self] in - guard let self = self, let aiId = data?.aiId else {return} - let vc = ChatBackgroundGridController() - vc.aiId = aiId - vc.selectBgUrl = data?.backgroundImg - let navc = CLNavigationController(rootViewController: vc) - navc.modalPresentationStyle = .fullScreen - self.viewController()?.present(navc, animated: true) - - if !self.config.chatBackgroundTapped{ - self.config.chatBackgroundTapped = true - AppCache.cache(key: CacheKey.chatRedBadgeConfig.rawValue, value: config) - self.tableView.reloadData() - NotificationCenter.post(name: .chatNaviMoreRedDotChanged) - } - } - return cell - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let view = ChatSettingSectionHeader() - if section == 0 { - view.label.text = "My Chat Persona" - } else { - view.label.text = "Chat Settings" - } - return view - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 56 - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView?.titleLabel) - } - - // MARK: Action - - func tapSelectChatModel(){ - guard let chatModels = AppDictManager.shared.chatModels, chatModels.count > 0 else{ - Hud.showIndicator() - AppDictManager.shared.loadChatModelDict { ok in - Hud.hideIndicator() - } - return - } - - let sheet = ChatModePickSheet(currentSelectedModelCode: data?.modelCode) - sheet.selectionCallback = { [weak self] selectedModel in - self?.updateChatModel(selectedModel) - } - sheet.show() - } - - private func updateChatModel(_ model: AIChatModel) { - guard let aiId = data?.aiId else { return } - - Hud.showIndicator() - var params = [String: Any]() - params.updateValue(aiId, forKey: "aiId") - params.updateValue(model.code ?? "", forKey: "code") - - AIRoleProvider.request(.chatModelSetup(params: params), modelType: EmptyModel.self) { [weak self] result in - Hud.hideIndicator() - switch result { - case .success: - // 更新本地数据 - self?.data?.modelCode = model.code - self?.data?.modelName = model.name - self?.tableView.reloadData() - // 发送通知更新其他页面 - NotificationCenter.post(name: .chatSettingUpdated) - case .failure: - break - } - } - } -} - -class ChatSettingChatPersonalCell: UITableViewCell { - var stackV = UIStackView() - - var nicknameItem = ChatSettingItemView() - var genderItem = ChatSettingItemView() - var ageItem = ChatSettingItemView() - var whoIm = ChatSettingItemView() - - var tapChatPersonAction: (() -> Void)? - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - stackV.backgroundColor = .c.csbn - stackV.axis = .vertical - stackV.layer.cornerRadius = 12 - stackV.layer.masksToBounds = true - contentView.addSubview(stackV) - stackV.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 24, bottom: 16, right: 24)) - } - - nicknameItem.titleLabel.text = "Nickname" - nicknameItem.contentLabel.text = "" - - genderItem.titleLabel.text = "Gender" - genderItem.contentLabel.text = "" - - ageItem.titleLabel.text = "Age" - ageItem.contentLabel.text = "" - - whoIm.title = "Who i am" - whoIm.bottomLine.isHidden = true - - stackV.addArrangedSubview(nicknameItem) - stackV.addArrangedSubview(genderItem) - stackV.addArrangedSubview(ageItem) - stackV.addArrangedSubview(whoIm) - - nicknameItem.tapTopButtonAction = { [weak self] in - self?.tapChatPersonAction?() - } - genderItem.tapTopButtonAction = { [weak self] in - self?.tapChatPersonAction?() - } - ageItem.tapTopButtonAction = { [weak self] in - self?.tapChatPersonAction?() - } - whoIm.tapTopButtonAction = { [weak self] in - self?.tapChatPersonAction?() - } - } - - func config(_ data: IMChatSetting?){ - guard let setting = data else {return} - - nicknameItem.contentLabel.text = setting.nickname - genderItem.contentLabel.text = setting.sex?.localizedText ?? Sex.noncomfirming.localizedText - var age = "-" - if let birthday = setting.birthday{ - let date = Date.dateFromMilliseconds(birthday) - let ageYears = Date().years(from: date) - age = "\(ageYears)" - } - ageItem.contentLabel.text = age - - if let whoAmI = setting.whoAmI, whoAmI.count > 0{ - whoIm.contentLabel.text = whoAmI - whoIm.contentLabel.textColor = .text - }else{ - whoIm.contentLabel.text = "Unfilled" - whoIm.contentLabel.textColor = .c.ctpd - } - } -} - -class ChatSettingOtherCell: UITableViewCell { - var stackV = UIStackView() - - var stackV1 = UIStackView() - var chatModelItem = ChatSettingItemView() - - var stackV2 = UIStackView() - var chatBubble = ChatSettingItemView() - var chatBackground = ChatSettingItemView() - - var stackV3 = UIStackView() - var autoPlayVoice = ChatSettingItemView() - - var data: IMChatSetting? - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - stackV.spacing = 16 - stackV.axis = .vertical - contentView.addSubview(stackV) - stackV.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 24, bottom: 16, right: 24)) - } - - stackV1.backgroundColor = .c.csbn - stackV1.layer.cornerRadius = 12 - stackV1.layer.masksToBounds = true - stackV1.axis = .vertical - stackV.addArrangedSubview(stackV1) - - stackV2.backgroundColor = .c.csbn - stackV2.layer.cornerRadius = 12 - stackV2.layer.masksToBounds = true - stackV2.axis = .vertical - stackV.addArrangedSubview(stackV2) - - stackV3.backgroundColor = .c.csbn - stackV3.layer.cornerRadius = 12 - stackV3.layer.masksToBounds = true - stackV3.axis = .vertical - stackV.addArrangedSubview(stackV3) - - chatModelItem.titleLabel.text = "Chat Model" - chatModelItem.contentLabel.text = "Role-playing" - chatModelItem.bottomLine.isHidden = true - - chatBubble.titleLabel.text = "Chat Bubble" - - chatBackground.title = "Chat Backgroud" - chatBackground.bottomLine.isHidden = true - - autoPlayVoice.title = "Auto play voice" - autoPlayVoice.bottomLine.isHidden = true - autoPlayVoice.setupSwitchView() - - stackV1.addArrangedSubview(chatModelItem) - stackV2.addArrangedSubview(chatBubble) - stackV2.addArrangedSubview(chatBackground) - stackV3.addArrangedSubview(autoPlayVoice) - } - - private func setupData(){ - - } - - private func setupEvent(){ - autoPlayVoice.controlSwitch.addTarget(self, action: #selector(autoPlayVoiceSwitchChanged), for: .valueChanged) - } - - func config(_ data: IMChatSetting?){ - self.data = data - guard let setting = data else {return} - - chatModelItem.contentLabel.text = setting.modelName ?? "" - autoPlayVoice.controlSwitch.isOn = (setting.isAutoPlayVoice ?? 0) == 1 - } - - @objc private func autoPlayVoiceSwitchChanged(_ sender: UISwitch){ - let isOn = sender.isOn - - if isOn && UserCore.shared.user?.isMember == false{ - AppRouter.goVIPCenter() - sender.isOn = false - return - } - - guard let aiId = data?.aiId else{return} - Hud.showIndicator() - AIRoleProvider.request(.chatSetAutoPlayVoice(aiId: Int(aiId), isAutoPlayVoice: isOn), modelType: EmptyModel.self) { result in - Hud.hideIndicator() - switch result { - case .success: - NotificationCenter.post(name: .chatSettingUpdated) - case .failure: - sender.isOn = !isOn - } - } - } -} - -class ChatSettingItemView: UIView { - var titleLabel: UILabel! - var badge: BadgeView! - - var arrow: UIImageView! - var contentLabel: UILabel! - var bottomLine: CLLine! - var topButton: UIButton! - var tapTopButtonAction: (() -> Void)? - - lazy var controlSwitch: UISwitch = { - let v = UISwitch() - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - var title: String? { - didSet { - titleLabel.text = title - } - } - - var content: String? { - didSet { - if let str = content, str.count > 0 { - contentLabel.text = content - contentLabel.textColor = .c.ctpn - } else { - contentLabel.text = "Unfilled" - contentLabel.textColor = .c.ctsn - } - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - snp.makeConstraints { make in - make.height.equalTo(64) - } - titleLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tll - v.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 755), for: .horizontal) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - badge = { - let v = BadgeView() - v.onlyShowPoint = true - v.badgeValue = 1 - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(titleLabel.snp.trailing).offset(8) - } - v.isHidden = true - return v - }() - - arrow = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .arrowRightBorder, size: CGSize(width: 12, height: 12), color: .c.ctsn) - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - return v - }() - - contentLabel = { - let v = UILabel() - v.font = .t.tbm - v.textColor = .text - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalTo(arrow.snp.leading).offset(-8) - make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(24) - } - return v - }() - - bottomLine = { - let v = CLLine() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.height.equalTo(0.5) - make.bottom.equalToSuperview() - } - return v - }() - - topButton = { - let v = UIButton() - v.addTarget(self, action: #selector(tapTopButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - } - - func setupSwitchView() { - contentLabel.isHidden = true - arrow.isHidden = true - topButton.isHidden = true - controlSwitch.isHidden = false - } - - @objc private func tapTopButton() { - tapTopButtonAction?() - } -} - -class ChatSettingSectionHeader: UIView { - var label: UILabel! - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .c.cbd - label = { - let v = UILabel() - v.textColor = .text - v.font = .t.tts - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(24) - } - return v - }() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/crush/Crush/Src/Modules/Chat/Util/GiftViewModel.swift b/crush/Crush/Src/Modules/Chat/Util/GiftViewModel.swift deleted file mode 100644 index 1cfb7ad..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/GiftViewModel.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// GiftViewModel.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import UIKit - -class GiftViewModel{ - -} diff --git a/crush/Crush/Src/Modules/Chat/Util/IMHelperUtil.swift b/crush/Crush/Src/Modules/Chat/Util/IMHelperUtil.swift deleted file mode 100644 index 6fcc56a..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/IMHelperUtil.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// IMHelperUtil.swift -// Crush -// -// Created by Leon on 2025/8/31. -// - -import Foundation - -// MARK: - IMHelperUtil -class IMHelperUtil{ - static func getAIIdByConversationId(_ conversationId: String?) -> Int?{ - guard let imConversationId = conversationId else{return nil} - - let stings = imConversationId.components(separatedBy: "|") - guard let last = stings.last else{return nil} - // let accountId = last - let strings2 = last.components(separatedBy:"@") - if let userIdStr = strings2.first { - if let userid = Int(userIdStr) { - return userid - } - } - - return nil - } -} - -// MARK: - IMAudioHelper -class IMAudioHelper{ - /// 提取用于语音的文本(去除括号内容和 emoji) - func extractTextForVoice(_ text: String) -> String { - var result = text - - // 去除括号内容(中英文括号) - let bracketPattern = #"\(.*?\)|(.*?)"# - if let regex = try? NSRegularExpression(pattern: bracketPattern, options: []) { - result = regex.stringByReplacingMatches(in: result, options: [], range: NSRange(location: 0, length: result.utf16.count), withTemplate: "") - } - - // 去除 emoji - // 使用 unicode 范围匹配 emoji - let emojiPattern = "[\\p{Emoji}]" - if let regex = try? NSRegularExpression(pattern: emojiPattern, options: []) { - result = regex.stringByReplacingMatches(in: result, options: [], range: NSRange(location: 0, length: result.utf16.count), withTemplate: "") - } - - return result.trimmingCharacters(in: .whitespacesAndNewlines) - } - - /// 计算文本的预估音频时长 - /// - Parameters: - /// - text: 文本内容 - /// - speechRate: 语速设置 (-50 到 50,默认为 0) - /// - baseWordsPerMinute: 基础语速(每分钟字数),默认为 200 - /// - Returns: 预估的音频时长(秒,TimeInterval) - func calculateAudioDuration( - text: String, - speechRate: Int = 0, - baseWordsPerMinute: Double = 200.0 - ) -> TimeInterval { - guard !text.isEmpty else { return 0 } - - let voiceText = extractTextForVoice(text) - guard !voiceText.isEmpty else { return 0 } - - // 中文字符数 - let chineseRegex = try! NSRegularExpression(pattern: "[\\u4e00-\\u9fff]") - let chineseCharCount = chineseRegex.numberOfMatches(in: voiceText, range: NSRange(location: 0, length: voiceText.utf16.count)) - - // 英文单词数(先去掉中文,再按单词匹配) - let textWithoutChinese = chineseRegex.stringByReplacingMatches(in: voiceText, range: NSRange(location: 0, length: voiceText.utf16.count), withTemplate: " ") - let wordRegex = try! NSRegularExpression(pattern: "\\b\\w+\\b") - let englishWordCount = wordRegex.numberOfMatches(in: textWithoutChinese, range: NSRange(location: 0, length: textWithoutChinese.utf16.count)) - - let totalWordCount = chineseCharCount + englishWordCount - if totalWordCount == 0 { return 0 } - - // 根据语速调整基础语速 - let speedMultiplier = 1.0 + (Double(speechRate) / 100.0) // -50 → 0.5, 50 → 1.5 - let adjustedWordsPerMinute = baseWordsPerMinute * speedMultiplier - - // 计算时长(分钟 → 秒) - let durationInMinutes = Double(totalWordCount) / adjustedWordsPerMinute - let durationInSeconds = durationInMinutes * 60.0 - - return max(0.5, durationInSeconds) - } - -// /// 格式化音频时长显示 -// /// - Parameter durationInSeconds: 时长(秒) -// /// - Returns: 格式化的时长字符串,如 "2''" 或 "1'23''" -// func formatAudioDuration(_ durationInSeconds: Double) -> String { -// if durationInSeconds < 1 { -// return "1''" -// } -// -// let totalSeconds = Int(round(durationInSeconds)) -// -// if totalSeconds < 60 { -// return "\(totalSeconds)''" -// } -// -// let minutes = totalSeconds / 60 -// let seconds = totalSeconds % 60 -// -// if seconds == 0 { -// return "\(minutes)'" -// } -// -// return String(format: "%d'%02d''", minutes, seconds) -// } - -} diff --git a/crush/Crush/Src/Modules/Chat/Util/IMImageContainerLogic.swift b/crush/Crush/Src/Modules/Chat/Util/IMImageContainerLogic.swift deleted file mode 100644 index 30ab348..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/IMImageContainerLogic.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// IMImageContainerLogic.swift -// Crush -// -// Created by Leon on 2025/8/22. -// - -import UIKit - -/// 设计稿中的常量 -struct ImageDisplayConstants { - var W_MAX: CGFloat - var H_MAX: CGFloat - var W_MIN: CGFloat - var H_MIN: CGFloat - var RATIO_MIN: CGFloat - var RATIO_MAX: CGFloat - - static let `default` = ImageDisplayConstants( - W_MAX: 372, - H_MAX: 372, - W_MIN: 48, - H_MIN: 48, - RATIO_MIN: 9.0 / 21.0, - RATIO_MAX: 21.0 / 9.0 - ) -} - -/// 计算结果 -struct CalculatedImageSize { - var width: CGFloat - var height: CGFloat - var type: ImageType - var ratio: CGFloat -} - -/// 图片类型 -enum ImageType: String { - case large - case small - case tiny -} - -/// 工具类 -class ImageDisplayLogic { - - /// 限制数值范围 - private static func clamp(_ value: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat { - return Swift.min(Swift.max(value, min), max) - } - - /// 判断图片类型 - static func getImageType(originalWidth: CGFloat, - originalHeight: CGFloat, - constants: ImageDisplayConstants = .default) -> ImageType { - let W_MAX = constants.W_MAX - let H_MAX = constants.H_MAX - let W_MIN = constants.W_MIN - let H_MIN = constants.H_MIN - - // A. 大图 - let isLargeImage = ( - (originalWidth >= W_MAX && originalHeight > (9.0/21.0) * H_MAX) || - (originalHeight >= H_MAX && originalWidth > (9.0/21.0) * W_MAX) - ) - if isLargeImage { return .large } - - // C. 超小图 - let isTinyImage = (originalWidth < W_MIN || originalHeight < H_MIN) - if isTinyImage { return .tiny } - - // B. 小图 - return .small - } - - /// 计算最终展示比例 - static func calculateDisplayRatio(originalWidth: CGFloat, - originalHeight: CGFloat, - imageType: ImageType, - constants: ImageDisplayConstants = .default) -> CGFloat { - let originalRatio = originalWidth / originalHeight - if imageType == .large { - return clamp(originalRatio, min: constants.RATIO_MIN, max: constants.RATIO_MAX) - } else { - return originalRatio - } - } - - /// 计算最终展示尺寸 - static func calculateImageDisplaySize(originalWidth: CGFloat, - originalHeight: CGFloat, - constants: ImageDisplayConstants = .default) -> CalculatedImageSize { - let W_MAX = constants.W_MAX - let H_MAX = constants.H_MAX - let W_MIN = constants.W_MIN - let H_MIN = constants.H_MIN - - // 1. 类型 - let imageType = getImageType(originalWidth: originalWidth, originalHeight: originalHeight, constants: constants) - - // 2. 比例 - let displayRatio = calculateDisplayRatio(originalWidth: originalWidth, originalHeight: originalHeight, imageType: imageType, constants: constants) - - // 3. 最终尺寸 - var finalWidth: CGFloat = 0 - var finalHeight: CGFloat = 0 - - switch imageType { - case .large: - if displayRatio >= 1 { - // 宽图 - finalWidth = W_MAX - finalHeight = W_MAX / displayRatio - } else { - // 高图 - finalHeight = H_MAX - finalWidth = H_MAX * displayRatio - } - case .small: - if displayRatio >= 1 { - finalHeight = originalHeight - finalWidth = min(originalWidth, W_MAX) - } else { - finalWidth = originalWidth - finalHeight = min(originalHeight, H_MAX) - } - case .tiny: - if displayRatio >= 1 { - finalHeight = H_MIN - finalWidth = min(H_MIN * displayRatio, W_MAX) - } else { - finalWidth = W_MIN - finalHeight = min(W_MIN / displayRatio, H_MAX) - } - } - - return CalculatedImageSize( - width: round(finalWidth), - height: round(finalHeight), - type: imageType, - ratio: displayRatio - ) - } - - /// 根据容器宽度调整常量 - static func adjustConstants(for containerWidth: CGFloat) -> ImageDisplayConstants { - let maxSize = min(containerWidth * 0.5, 372) - var c = ImageDisplayConstants.default - c.W_MAX = maxSize - c.H_MAX = maxSize - return c - } -} diff --git a/crush/Crush/Src/Modules/Chat/Util/IMManager.swift b/crush/Crush/Src/Modules/Chat/Util/IMManager.swift deleted file mode 100644 index d7b79e3..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/IMManager.swift +++ /dev/null @@ -1,524 +0,0 @@ -// -// IMManager.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import NIMSDK -import Combine -import UserNotifications - -class IMManager: NSObject { - public static let shared = IMManager() - - var retryCount = 0 - // appkey信息 - var imInfo: IMConfigInfo? - - // 给oc用的 - var accountId: String { - imInfo?.accountId ?? "" - } - - // session Cache - var cacheSessions = [String]() - - // Config是否需要发送通知 - var notNeedSendNotification = false - - // UnreadCount 未读消息 - var totalUnreadCount: Int { - return chatTabAllUnreadCount - } - - /// 🔥 chat tab 的未读总数 - @Published var chatTabAllUnreadCount: Int = 0 - - /// 🔥notice center 入口的未读数 - @Published var noticeUnreadCount: Int = 0 - @Published var noticeStat: MessageStat? - - /// 🔥IM 消息未读数 - @Published var sessionUnreadCount: Int = 0 - - private var cancellables = Set() - - override init() { - super.init() - setupDefaultConfig() - setupEvent() - } - - - func setupEvent() { - NotificationCenter.default.addObserver(self, selector: #selector(notiLoginSuccess), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notiLogout), name: AppNotificationName.userLogout.notificationName, object: nil) - - // -// UserCore.shared.isLogin() - - Publishers.CombineLatest($noticeUnreadCount, $sessionUnreadCount) - .map { $0 + $1 } - .assign(to: \.chatTabAllUnreadCount, on: self) - .store(in: &cancellables) - - $chatTabAllUnreadCount.sink { count in - UIApplication.shared.applicationIconBadgeNumber = count - }.store(in: &cancellables) - } - - // sdk配置基础 - func setupDefaultConfig() { - let config = NIMSDKConfig.shared() - - // config.delegate = self - // 同步多端未读数 - config.shouldSyncUnreadCount = true - // 链接失败重试次数 - config.maxAutoLoginRetryTimes = 10 - // 本地log存储天数 - config.maximumLogDays = 30 - // 是否将群通知计入未读数 - config.shouldCountTeamNotification = true - // 是否支持动图缩略 - config.animatedImageThumbnailEnabled = false - // 是否在收到消息后自动下载附件 - config.fetchAttachmentAutomaticallyAfterReceiving = false - // 是否在收到聊天室消息后下载附件 - config.fetchAttachmentAutomaticallyAfterReceivingInChatroom = true - // 是否开启异步获取最近会话 - config.asyncLoadRecentSessionEnabled = true - // 禁止后台重连,退到后台即断开 - config.reconnectInBackgroundStateDisabled = true - // 置顶会话同步开关。如果不使用置顶功能建议关闭(默认),有利于节省流量 - config.shouldSyncStickTopSessionInfos = false - } - - /// 🔥启动初始化SDK,主要入口 - public func setupNIM() { - - NIMSDK.shared().v2LoginService.add(self) - NIMSDK.shared().v2ConversationService.add(self) - NIMSDK.shared().v2SubscriptionService.add(self) - NIMSDK.shared().v2MessageService.add(self) - - // 初始化SDK - v2InitailSDK() - - refreshIMServerInfoConfig() - //setupNIMAppKey() - } - - func v2InitailSDK(){ -// guard UserCore.shared.isLogin() else { -// return -// } - - let appkey = "2d6abc076f434fc52320c7118de5fead" - - var option = NIMSDKOption.init(appKey: appkey) -#if DEBUG - option.apnsCername = "developerpush" -#else - option.apnsCername = "distributionpush" -#endif - let v2Option = V2NIMSDKOption() - v2Option.enableV2CloudConversation = true - - NIMSDK.shared().register(withOptionV2: option, v2Option: v2Option) - - if NIMSDK.shared().loginManager.isLogined(){ - dlog("💬❌ NIMSDK 已登录") - } - - // v2AutoLogin() - } - - func v2AutoLogin(){ - if imInfo == nil { - // 从本地取 - if let info = AppCache.fetchCache(key: CacheKey.ImAppkeyInfo.rawValue, type: IMConfigInfo.self) { - imInfo = info - } else { - return - } - } - - guard UserCore.shared.isLogin() else { return } - guard let info = imInfo else { return } - guard let account = info.accountId else { return } - guard let imToken = info.token else { return } - let option = V2NIMLoginOption() - option.retryCount = 3 - NIMSDK.shared().v2LoginService.login(account, token: imToken, option: option) {[weak self] in - // dlog("☁️Log Success") - self?.regetConversationAllUnreadCount() - } - } - - func logoutIM(){ - NIMSDK.shared().v2LoginService.logout { - dlog("☁️Log out") - } failure: { error in - dlog("☁️Log error: \(error)") - } - - } - - /// 清理当前账号的IM登录信息 - private func clearLogData() { - resetCount() - UserCore.shared.logout() - NotificationCenter.post(name: .presentSignInVc, object: nil, userInfo: nil) - } - - func handleReceiveCreate(recentSession: V2NIMConversation) { - guard let message = recentSession.lastMessage else { return } - dealNotice(message: message) - } - - /// 全局收到消息,针对性处理一些消息 - func handleReveiceUpdate(recentSession: V2NIMConversation) { - guard let message = recentSession.lastMessage else { return } - if let remoteExt = message.serverExtension{ -// if let type = remoteExt["type"] as? String{ -// if type == MessageTypeStringCustom || type == MessageTypeStringNormal, let session = recentSession.session{ -// // 打电话的消息,交给打电话处理 -// dealPhoneCallMessage(message: message, session: session) -// } -// } - }else if message.messageType == .MESSAGE_TYPE_CUSTOM { - // 自定义消息,暂不处理 - } - - if message.messageType == .MESSAGE_TYPE_CUSTOM{ - - }else{ - dealNotice(message: message) - } - } - - func dealNotice(message: V2NIMLastMessage) { - // NIMMessage - guard UIApplication.shared.applicationState == .active else { - return - } - let generator = UIImpactFeedbackGenerator(style: .medium) - generator.impactOccurred() - - } -} - -// loadData -extension IMManager { - /// 刷新app 信息 - func refreshIMServerInfoConfig() { - guard UserCore.shared.isLogin() else { - return - } - guard retryCount <= 5 else { - // 重试最多五次 - return - } - - IMProvider.request(.getIMAccount, modelType: IMConfigInfo.self) {[weak self] result in - switch result { - case let .success(model): - if model != nil { - self?.imInfo = model - AppCache.cache(key: CacheKey.ImAppkeyInfo.rawValue, value: model) - //self?.setupNIMAppKey() - self?.v2AutoLogin() // - self?.retryCount = 0 - } - if String.realEmpty(str: model?.accountId) { - // 控制重试时间 - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self?.refreshIMServerInfoConfig() - self?.retryCount += 1 - } - } - case let .failure(error): - dlog(error) - // 控制重试时间 - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self?.refreshIMServerInfoConfig() - self?.retryCount += 1 - } - } - } - } - - public func retryRefreshConfig() { - if NIMSDK.shared().loginManager.isLogined() { - return - } - // 重试登录 - retryCount = 0 - refreshIMServerInfoConfig() - } - -} - -// MARK: 🔴Unread Count 未读数和红点处理 - -extension IMManager { - - public func regetAllUnreadCount(){ - regetNoticeUnread() - regetConversationAllUnreadCount() - } - - /// 请求业务系统上的未读数 - func regetNoticeUnread(completion:((Bool, MessageStat?)-> Void)? = nil){ - guard UserCore.shared.isLogin() else{ - completion?(false, nil) - return - } - IMProvider.request(.messageStat, modelType: MessageStat.self) {[weak self] result in - switch result { - case .success(let model): - self?.noticeStat = model - self?.noticeUnreadCount = model?.unRead ?? 0 - completion?(true, model) - case .failure: - break - } - } - } - - - public func regetConversationAllUnreadCount(){ - // 获取未读数 - sessionUnreadCount = NIMSDK.shared().v2ConversationService.getTotalUnreadCount() - dlog("🚩IMManager reget unread count:\(sessionUnreadCount)") - } - - func registerNIMCount(){ - let filter = V2NIMConversationFilter() - filter.conversationTypes = [1]//V2NIMConversationType.CONVERSATION_TYPE_P2P - filter.ignoreMuted = true - NIMSDK.shared().v2ConversationService.subscribeUnreadCount(by: filter) - // Result can see in onUnreadCountChangedByFilter - } - - /// 🔥conversationIds - func clearUnreadCountBy(ids:[String]){ - guard ids.count > 0 else{ return } - NIMSDK.shared().v2ConversationService.clearUnreadCount(byIds: ids) {[weak self] resuls in - let getUnreadcount = NIMSDK.shared().v2ConversationService.getTotalUnreadCount() - // dlog("💬getTotalUnreadCount: \(getUnreadcount)") - self?.sessionUnreadCount = getUnreadcount - } - } - - func clearAllUnread(){ - NIMSDK.shared().v2ConversationService.clearTotalUnreadCount { - dlog("☁️Clear all unread message ok✅") - } - } - - // 重置未读数 - func resetCount() { - notNeedSendNotification = true - sessionUnreadCount = 0 - noticeUnreadCount = 0 - notNeedSendNotification = false - - } - -} - -extension IMManager: V2NIMLoginListener{ - func onLoginStatus(_ status: V2NIMLoginStatus) { - dlog("☁️login status:\(status)") - if status == .LOGIN_STATUS_LOGINED{ - // 登录成功 - dlog("☁️login success") - // 注册未读数 - registerNIMCount() - - regetConversationAllUnreadCount() - - regetNoticeUnread() - } - } - - - - func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) { - // 判断是否登录 - guard UserCore.shared.isLogin() else { - return - } - - let reason = "Your account has signed in through another device" - - - let alert = Alert(title: "Session Interrupted", text: reason) - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) {[weak self] in - self?.clearLogData() - } - alert.addAction(action1) - alert.show() - } - - func onLoginFailed(_ error: V2NIMError) { - dlog("☁️onAutoLoginFailed__ \(error.description)") - if error.code == 417 { - guard let _ = imInfo?.accountId, let _ = imInfo?.token else { return } - v2AutoLogin() - } - } -} - -// MARK: NIMConversationManagerDelegate - -extension IMManager: V2NIMConversationListener{ - func onSyncStarted() { - dlog("☁️onSyncStarted") - } - - func onSyncFinished() { - dlog("☁️onSyncFinished") - } - - func onSyncFailed(_ error: V2NIMError) { - dlog("☁️onSyncFailed:\(error)") - } - - func onConversationCreated(_ conversation: V2NIMConversation) { - handleReveiceUpdate(recentSession: conversation) - } - - func onConversationChanged(_ conversations: [V2NIMConversation]) { - for per in conversations{ - handleReveiceUpdate(recentSession: per) - } - } - - func onConversationDeleted(_ conversationIds: [String]) { - dlog("💬🗑️onConversationDeleted: \(conversationIds)") - } - - func onTotalUnreadCountChanged(_ unreadCount: Int) { - dlog("💬onTotalUnreadCountChanged : \(unreadCount)") - sessionUnreadCount = unreadCount - } - - // 账号多端登录会话已读时间戳标记通知回调 - func onConversationReadTimeUpdated(_ conversationId: String, readTime: TimeInterval) { - - } - - func onUnreadCountChanged(by filter: V2NIMConversationFilter, unreadCount: Int) { - // dlog("☁️unreadCount:\(unreadCount)") - } -} - -extension IMManager : V2NIMSubscribeListener{ - /// 用户状态变化 - func onUserStatusChanged(_ data: [V2NIMUserStatus]) { - // none - dlog("☁️onUserStatusChanged:\(data)") - } -} - -extension IMManager : V2NIMMessageListener{ - func onReceive(_ messages: [V2NIMMessage]) { - // ... - regetConversationAllUnreadCount() - - } -} - -// MARK: Notification -extension IMManager { - @objc func notiLoginSuccess() { - retryCount = 0 - refreshIMServerInfoConfig() - - - UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { granted, error in - print("Notification granted: \(granted), error: \(String(describing: error))") - } - } - - @objc func notiLogout() { - imInfo = nil - resetCount() - removeAllCache() - NIMSDK.shared().v2LoginService.logout { [weak self] in - self?.resetCount() - } - } -} - -// public cache -extension IMManager { - public func addCache(sessionID: String) { - if !cacheSessions.contains(sessionID) { - cacheSessions.append(sessionID) - } - } - - public func deleteCache(sessionID: String) { - if cacheSessions.contains(sessionID) { - cacheSessions.removeObj(sessionID) - } - } - - func findCache(sessionID: String) -> Bool{ - return cacheSessions.contains(sessionID) - } - - func removeAllCache() { - cacheSessions.removeAll() - } -} - -// 消息处理 -extension IMManager { - - /// 发送消息, 注意这里传云信的account ID,不是用户userID - static func sendMessage(msgText: String, accID:String) { - - if String.realEmpty(str: msgText.trimmed) { - // 为空,不做操作 - return - } - // 创建消息 - let message = IMMessageMaker.msgWithText(msgText) - // 判断消息是否可以发送 - let canSend = IMManager.dealWillSendMessage(message: message) - if canSend { - // 创建会话 -// let session = NIMSession(accID, type: .P2P) -// NIMSDK.shared().chatManager.send(message, to: session) { error in -// // 消息发送回调 -// #warning("test action") -// } - } else { - // 这里需不需要处理成本地消息? - } - } - - // 处理是否能发送消息 - static public func dealWillSendMessage(message: V2NIMMessage) -> Bool { -// var dict = [String:String]() - -// if message.messageType == .text { -// // 文本消息,检查敏感词 -// let word = GameDataManager.shared.matchKeywordWith(sourceStr: message.text?.lowercased() ?? "") -// if word.count > 0 { -// dict["keyword"] = word -// dict["type"] = "NOTICE_KEYWORD" -// message.remoteExt = dict -// return false -// } -// } - - return true - } -} diff --git a/crush/Crush/Src/Modules/Chat/Util/IMMessageMaker.swift b/crush/Crush/Src/Modules/Chat/Util/IMMessageMaker.swift deleted file mode 100644 index 035aca1..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/IMMessageMaker.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// IMMessageMaker.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import UIKit -import NIMSDK - -/// NIMMessage maker -class IMMessageMaker { - - static func msgWithText(_ text: String) -> V2NIMMessage { - let textMessage = V2NIMMessageCreator.createTextMessage(text) - return textMessage - } - - static func msgWithTips(_ text: String) -> V2NIMMessage { - let tipMessage = V2NIMMessageCreator.createTipsMessage(text) - return tipMessage - } - - static func msgWithCustom(_ text: String?, attachMent: String?) -> V2NIMMessage { - let msg = V2NIMMessageCreator.createCustomMessage(text ?? "", rawAttachment: attachMent ?? "") - return msg - } - - static func msgWithCustom(_ text: String? = "", attachment: IMCustomAttachment?) -> V2NIMMessage { - - guard let attach = attachment, let jsonString = CodableHelper.encodeToJSONString(attach) else{ - assert(false) - return V2NIMMessage() - } - - let msg = V2NIMMessageCreator.createCustomMessage(text ?? "", rawAttachment: jsonString) - - return msg - } -} diff --git a/crush/Crush/Src/Modules/Chat/Util/IMRemoteUtil.swift b/crush/Crush/Src/Modules/Chat/Util/IMRemoteUtil.swift deleted file mode 100644 index b05cda9..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/IMRemoteUtil.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// IMRemoteUtil.swift -// Crush -// -// Created by Leon on 2025/8/18. -// - -import NIMSDK -import UIKit -class IMRemoteUtil { - /// V2NIMMessage -> IMBaseRemoteInfo 初步划分确定 消息划分的cellType - static func dealRemoteInfo(message: V2NIMMessage?) -> IMBaseRemoteInfo { - guard let msg = message else { - return IMBaseRemoteInfo() - } - - // Parse serverExtension - let remote = message?.serverExtension ?? "" - let info = CodableHelper.decode(IMBaseRemoteInfo.self, from: remote) ?? IMBaseRemoteInfo() - info.displayString = message?.text - - // Set default cellType according to Message type. - switch message?.messageType { - case .MESSAGE_TYPE_TEXT: - if msg.isSelf { - info.cellType = .text - } else { - info.cellType = .aimsg - } - case .MESSAGE_TYPE_TIP: - info.cellType = .tips - case .MESSAGE_TYPE_CUSTOM: - // Parse 自定义消息的cellType,和Attachment - dealCustom(info: info, attachment: message?.attachment) - default: - info.cellType = .unknown - } - - return info - } - - private static func dealCustom(info: IMBaseRemoteInfo, attachment: V2NIMMessageAttachment?) { - guard let attach = attachment else { - return - } - info.cellType = .text - let attachmentString = attach.raw - - // dlog("☁️🔥 V2NIMMessageAttachment's raw:\(attach.raw)") - - guard attachmentString.count > 0, let model = CodableHelper.decode(IMCustomAttachment.self, from: attachmentString) else { - return - } - - info.customAttachment = model - guard let type = model.type else { - dlog("☁️❌ wrong type, attach.raw is: \(attachmentString)") - return - } - - switch type { - case .IMAGE: - info.cellType = .image - info.displayString = "[Image]" - case .IM_SEND_GIFT: - info.cellType = .gift - info.displayString = "[Gift]" - case .CALL: - info.cellType = .phonecall - info.displayString = model.getDisplayString() - default: - break - } - } - - // MARK: - Helper - - // MARK: Call - - -} diff --git a/crush/Crush/Src/Modules/Chat/Util/IMUserKit.swift b/crush/Crush/Src/Modules/Chat/Util/IMUserKit.swift deleted file mode 100644 index c549c39..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/IMUserKit.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// IMUserKit.swift -// Crush -// -// Created by Leon on 2025/8/21. -// - -import Foundation - -class IMUserKit { - /// 一般在扩展字段里 - var userId: Int? - var accountId: String? - var nickname: String? - var avatar: String? - - var user: V2NIMUser? - - var loadCompleteAction: ((_ user: IMUserKit) -> Void)? - - class func imUserKitWith(accId: String?, completion: ((_ user: IMUserKit?) -> Void)? = nil) -> IMUserKit { - let kit = IMUserKit() - // let conversationId = V2NIMConversationIdUtil.p2pConversationId(accountId) - guard let accountId = accId, accountId.count > 0 else { - return kit - } - - // NIMSDK.shared().v2UserService.getUserInfo(accountId, error:) - NIMSDK.shared().v2UserService.getUserList([accountId]) { user in - if let searchUser = user.first { - kit.user = searchUser - kit.syncDataFrom(nimUser: searchUser) - completion?(kit) - kit.loadCompleteAction?(kit) - - // dlog("☁️IMUserKit user(V2NIMUser) get:\(user)") - } else { - //fatalError("wrong state") - dlog("☁️IMUserKit ❌user nil") - } - } failure: { error in - dlog("☁️IMUserKit ❌ get user list error:\(error)") - } - - return kit - } - - class func accountIdWithAIId(aiId:Int) -> String?{ - if aiId <= 0{ - return nil - } - if APIConfig.environment == .appStore || APIConfig.environment == .product{ - return "\(aiId)@r" - }else{ - // Test Envirment - return "\(aiId)@r@t" - } - } - - private func syncDataFrom(nimUser: V2NIMUser?) { - guard let imUser = nimUser else { - return - } - - accountId = imUser.accountId - nickname = imUser.name - avatar = imUser.avatar - } -} diff --git a/crush/Crush/Src/Modules/Chat/Util/SessionUtil.swift b/crush/Crush/Src/Modules/Chat/Util/SessionUtil.swift deleted file mode 100755 index 110a968..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/SessionUtil.swift +++ /dev/null @@ -1,565 +0,0 @@ -// -// SessionUtil.swift -// LegendTeam -// -// Created by 梁博 on 20/12/21. -// - -import Foundation -import NIMSDK -class SessionUtil { - - var topViewHeight: CGFloat = 0 - let mssagePageSize = 50 - //var session: NIMSession! - //var conversation: V2NIMConversation! - var conversationId : String! - - let showTimeInterval: TimeInterval = 180 - - var cellModels: [SessionBaseModel]! = [SessionBaseModel]() - /// Key: messageClientId。 ⚠️不能用messageServerId,因为消息可能是在发送中,导致没有messageServerId - var msgIdDict: [String: SessionBaseModel]! = [String: SessionBaseModel]() - - func getLastMessageModel() -> SessionBaseModel? { - var lastModel: SessionBaseModel? - for model in cellModels { - if let messageServerId = model.v2msg?.messageServerId, messageServerId.count > 0{ - lastModel = model - } - } - return lastModel - } -} - -// private -extension SessionUtil { - /// 最晚的时间, 也是排在最前面的时间 - func firtTimeStamp() -> TimeInterval { - guard let model = cellModels.first else { - return 0 - } - return model.timeStamp - } - - /// 最后一条消息的时候,也是离现在最近的时间 - func lastTimeInterval() -> TimeInterval { - return cellModels.last?.timeStamp ?? 0 - } - - /// 是否能追加时间显示 - func shouldAppendTimestamp(model: SessionBaseModel) -> Bool { - if model.cellType == .aiLoading{ - return false - } - return model.timeStamp - lastTimeInterval() > showTimeInterval - } - - /// 是否能插入时间显示 - func shouldInsertTimestamp(model: SessionBaseModel) -> Bool { - return firtTimeStamp() - model.timeStamp > showTimeInterval - } - - /// 是否需要忽略本条消息,不创建model,不显示 - func ignore(info: IMBaseRemoteInfo) -> Bool{ - var ignore = false - if let customAttachment = info.customAttachment{ - if let type = customAttachment.type{ - if type == .HEARTBEAT_LEVEL_DOWN || - type == .HEARTBEAT_LEVEL_UP || - type == .INSUFFICIENT_BALANCE || - type == .VOICE_CHAT_CONTENT - { - ignore = true - } - }else{ - // 没有type的自定义消息 - ignore = true - } - } - - return ignore - } - - /// 是否需要追加一个model显示,判断一条消息显示两个model的情况 - func needDeriveModel(message: V2NIMMessage) -> Bool { - if message.messageType == .MESSAGE_TYPE_CUSTOM{ - if let attachment = message.attachment?.raw, attachment.count > 0 { - let model = CodableHelper.decode(IMCustomAttachment.self, from: attachment) - if let type = model?.type, type.rawValue.uppercased() == CLCustomAttchType.IMAGE.rawValue{ - return true - } - } - - } - - if message.serverExtension == nil { - return false - } - let info = IMRemoteUtil.dealRemoteInfo(message: message) - if info.cellType == .keyword - || info.cellType == .gift - { - return true - } - return false - } - - func needDecriveModel(with: IMBaseRemoteInfo) -> Bool{ - if let customAttachment = with.customAttachment, let type = customAttachment.type{ - if type == .IMAGE{ - return true - } - } - - if with.cellType == .keyword || with.cellType == .gift{ - return true - } - return false - } - - /// 将model存在字典中 - func setIdIdctObject(model: SessionBaseModel) { - if model.isDeriveModel { - return - } - - guard let messageID = model.v2msg?.messageClientId else { return } - msgIdDict[messageID] = model - } - - /// message是否已经存在model - func modelIsExist(model: SessionBaseModel) -> Bool { - guard let messageID = model.v2msg?.messageClientId else { return false } - return msgIdDict[messageID] != nil - } - - func logAllCellModels(){ - if APIConfig.environment == .test{ - var string = "☁️cellModels(\(cellModels.count):\n" - for per in cellModels { - string += "\(per)\n" - } - dlog(string) - } - } -} - -// MARK: - Deal Models -/// Deal Models -extension SessionUtil { - /// 根据类型创建cellModel - func createCellModelWith(type: SessionCellType) -> SessionBaseModel { - let model = SessionBaseModel() - let configType = SessionBaseModel.getContentConfigClass(type: type) - // 创建配置类 - model.config = configType.init() - model.cellType = type - - switch type { - case .image: - model.shouldShowBubble = false - case .gift: - // 卡片 - model.shouldShowBubble = false - model.shouldShowCardView = true - case .timeStamp,.tips, .create, .keyword, .phonecall: - model.shouldShowBubble = false - model.shouldShowTips = true - default: - // 默认bubble - model.shouldShowBubble = true - model.shouldShowTips = false - break - } - return model - } - - func createCellModelWithCustom(attachment:String?) -> SessionBaseModel{ - let model = SessionBaseModel() - - guard let raw = attachment, let data = CodableHelper.decode(IMCustomAttachment.self, from: raw) else{ - return model - } - -// switch data.type{ -// case .IMAGE: break -// -// } - - //let configType = SessionBaseModel.getContentConfigClass(type: type) - - - return model - } - - /// 处理查询回来的消息 - func dealMessages(messages: [V2NIMMessage]?, completion: @escaping ((_ messageDatas: [V2NIMMessage]?, _ error: Error?) -> Void)) { - var tempModels = modelWithV2(datas: messages!) - var tempMessages = messages ?? [V2NIMMessage]() - -// while tempModels.count < mssagePageSize, messages!.count > 0 { -// tempMessages = NIMSDK.shared().conversationManager.messages(in: session, message: tempMessages.first, limit: mssagePageSize)! -// if Array.realEmpty(array: tempMessages) { -// break -// } -// let theModels = modelWith(messsages: tempMessages) -// tempModels.insert(contentsOf: theModels, at: 0) -// } - - -// let indexs = insertMessagesWith(models: tempModels.reversed()) - DispatchQueue.main.async { - completion( messages, nil) - } - } - - @discardableResult - func inserMessageWith(message: V2NIMMessage, atIndex: Int) -> [Int]{ - let models = modelWithV2(datas: [message]) - guard let model = models.first else { return [] } - self.cellModels.insert(model, at: atIndex) - return [atIndex] - } - - /// 追加model到数据源 - func appendWith(models: [SessionBaseModel]) -> [Int] { - if Array.realEmpty(array: models) { - return [] - } - - let count = cellModels.count - for model in models { - if modelIsExist(model: model), !model.isDeriveModel { - continue - } - _ = appendWith(model: model) - } - - var indexs = [Int]() - for i in count ..< cellModels.count { - indexs.append(i) - } - return indexs - } - - - /// 追加model到数据源 - @discardableResult - func appendWith(model: SessionBaseModel) -> [Int] { - var indexs = [Int]() - if shouldAppendTimestamp(model: model) { - addTimestamp(model: model, index: cellModels.count) - indexs.append(cellModels.count) - } - - cellModels.insert(model, at: cellModels.count) - indexs.append(cellModels.count) - setIdIdctObject(model: model) - - return indexs - } - - /// 追加model到数据源 - func addTimestamp(model: SessionBaseModel, index: Int) { - let timeModel = createCellModelWith(type: .timeStamp) - timeModel.shouldShowBubble = false - timeModel.shouldShowTips = true - timeModel.timeStamp = model.v2msg?.createTime ?? model.timeStamp - cellModels.insert(timeModel, at: index) - } - - /// 通过消息查找当前model - func findCellModel(message: V2NIMMessage?) -> SessionBaseModel? { - guard let messageID = message?.messageClientId else { return nil } - // 获取字典里的model数据 - let model = msgIdDict[messageID] - return model - } - - /// 查询model在数据源中的位置index - func modelIndexWith(model: SessionBaseModel) -> Int? { - for i in 0 ..< cellModels.count { - let obj = cellModels[i] - //if obj.isEqual(model) { - if obj === model{ - return i - } - } - return nil - } - - /// 通过message 追加model 到数据源 - @discardableResult - func appendWith(messages: [V2NIMMessage]) -> [Int] { - let models = modelWithV2(datas: messages) - // modelWithV2 生成的是反序的 - let indexs = appendWith(models: models.reversed()) - return indexs - } - - /// 通过message 更新数据源的 model - func updateWith(message: V2NIMMessage) -> [Int]? { - guard let model = findCellModel(message: message) else { return nil } - guard let index = modelIndexWith(model: model) else { return nil } - - var indexs = [Int]() - indexs.append(index) - if needDeriveModel(message: message) { - if index + 1 < cellModels.count { - indexs.append(index + 1) - } - } - - return indexs - } - - // MARK: V2 - /// [V2NIMMessage] -> [SessionBaseModel] - func modelWithV2(datas:[V2NIMMessage]) -> [SessionBaseModel]{ - var array = [SessionBaseModel]() - - for i in 0 ..< datas.count { - let message = datas[i] - - if let messageID = message.messageClientId, msgIdDict[messageID] != nil{ - // 已经存在了 - dlog("☁️⚠️⚠️\(message)已经存在了,应该更新,而不是新创建!⚠️⚠️") - continue - } - - if let serverExtension = message.serverExtension, serverExtension.count > 0{ - dlog("☁️message___ext___\(serverExtension)") - } - - // Step 通过消息获取初步的cellType - let info = IMRemoteUtil.dealRemoteInfo(message: message) - - // 需要被忽略掉的消息 - guard !ignore(info: info) else { - continue - } - - // Step 通过类型创建对应的cellModel - if needDecriveModel(with: info) { // needDeriveModel(message: message) - // 从下往上显示: - // 消息主体(显示在上面👆cell的上方)// 消息主体 - let cellModel = createCellModelWith(type: info.cellType) - cellModel.baseRemoteInfo = info - cellModel.isDeriveModel = true - cellModel.shouldShowLeft = !message.isSelf - cellModel.v2msg = message - cellModel.timeStamp = message.createTime - array.append(cellModel) - - if message.messageType == .MESSAGE_TYPE_TEXT { - if message.isSelf{ - let cellModel = createCellModelWith(type: .text) - cellModel.baseRemoteInfo = info - cellModel.shouldShowLeft = false - cellModel.v2msg = message - cellModel.timeStamp = message.createTime - array.append(cellModel) - }else{ - let cellModel = createCellModelWith(type: .aimsg) - cellModel.baseRemoteInfo = info - cellModel.shouldShowLeft = true - cellModel.v2msg = message - cellModel.timeStamp = message.createTime - array.append(cellModel) - } - } else if message.messageType == .MESSAGE_TYPE_TIP { - let cellModel = createCellModelWith(type: .tips) - cellModel.baseRemoteInfo = info - cellModel.shouldShowLeft = !message.isSelf - cellModel.v2msg = message - cellModel.timeStamp = message.createTime - array.append(cellModel) - }else if message.messageType == .MESSAGE_TYPE_CUSTOM{ - // 自定义消息,承载很多业务消息类型 - if message.isSelf{ - let cellModel = createCellModelWith(type: .text) - cellModel.baseRemoteInfo = info - cellModel.shouldShowLeft = !message.isSelf - cellModel.v2msg = message - cellModel.timeStamp = message.createTime - array.append(cellModel) - }else{ - if info.cellType != .image{ - let cellModel = createCellModelWith(type: .aimsg) - cellModel.baseRemoteInfo = info - cellModel.shouldShowLeft = !message.isSelf - cellModel.v2msg = message - cellModel.timeStamp = message.createTime - array.append(cellModel) - } - } - } - - - - - } else { - // 不需要附加一个cell,正常添加 - let cellModel = createCellModelWith(type: info.cellType) - cellModel.baseRemoteInfo = info - cellModel.shouldShowLeft = !message.isSelf - cellModel.v2msg = message - cellModel.timeStamp = message.createTime - array.append(cellModel) - } - } - - - return array - } - - func createAnAILoadingModel(){ - let cellModel = createCellModelWith(type: .aiLoading) - cellModel.isDeriveModel = true - cellModel.shouldShowLeft = true - cellModel.v2msg = nil - cellModel.timeStamp = TimeInterval(Date().timeStampInSeconds) - appendWith(model: cellModel) - } - - func clearAILoadingModel(){ - cellModels.removeAll(where: {$0.cellType == .aiLoading}) - } - - - @discardableResult - func insertMessagesWith(models: [SessionBaseModel]) -> [Int] { - if Array.realEmpty(array: models) { - return [] - } - - let count = cellModels.count - - for i in 0 ..< models.count { - let model = models[i] - if modelIsExist(model: model), !model.isDeriveModel, !model.shouldShowTips { - continue - } - insertMessageWith(model: model, needTime: cellModels.count != count) - } - - var indexs = [Int]() - for i in 0 ..< cellModels.count - count { - indexs.append(i) - } - return indexs - } - - /// 插入model到数据源 - func insertMessageWith(model: SessionBaseModel, needTime: Bool) { - // 这条消息上面不能有时间戳,下面必须要有时间戳 - let isCreateTimestampCell = (model.cellType == .create || model.cellType == .tips) - if isCreateTimestampCell { - if let theModel = cellModels.first, theModel.cellType != .timeStamp { - addTimestamp(model: theModel, index: 0) - } - } else { - if !shouldInsertTimestamp(model: model), needTime { - let model = cellModels.first - if model?.cellType == .timeStamp { - cellModels.removeFirst() - } - } - } - - cellModels.insert(model, at: 0) - setIdIdctObject(model: model) - if !isCreateTimestampCell { - addTimestamp(model: model, index: 0) - } - } - - func loadMessages(completion: @escaping ((_ firstPage: Bool, _ messageDatas: [V2NIMMessage]?, _ error: V2NIMError?) -> Void)){ - let option = V2NIMMessageListOption() - - var loadMoreMsgFlag = false - if let v2Msg = getLastMessageModel()?.v2msg{ - option.anchorMessage = v2Msg - option.beginTime = v2Msg.createTime - loadMoreMsgFlag = true - } - - option.conversationId = conversationId - NIMSDK.shared().v2MessageService.getMessageList(option) {[weak self] messages in - if let models = self?.modelWithV2(datas: messages){ - // self?.cellModels = models - self?.insertMessagesWith(models: models) - completion(!loadMoreMsgFlag, messages,nil) - } - } failure: { error in - dlog("☁️Get messages error:\(error)") - completion(!loadMoreMsgFlag, nil,error) - } - - - } -} - -// MARK: - Messages -extension SessionUtil { - /// 发送消息 - func sendMessage(message: V2NIMMessage) { - sendMessage(message: message) { error in - print(error ?? "") - } - } - - /// 发送消息 - func sendMessage(message: V2NIMMessage, completion: @escaping ((_ error: Error?) -> Void)) { - // See more params in https://doc.yunxin.163.com/messaging2/client-apis/DAxNjk0Mzc?platform=client#V2NIMSendMessageParams - let param = V2NIMSendMessageParams() - - NIMSDK.shared().v2MessageService.send(message, conversationId: conversationId, params: param) { result in - dlog("☁️✅send message ok \(message.text ?? "")") - } failure: { error in - dlog("☁️❌send message failed: \(error)") - if error.code == 20000{ - CLPurchase.shared.showChatModelsAndIAPEntrySheet() - } - } - -// NIMSDK.shared().chatManager.send(message, to: session) { error in -// completion(error) -// } - } - - -} - -// MARK: - Test -extension SessionUtil{ - func testModes(completion: @escaping (() -> Void)) { - var array = [SessionBaseModel]() - do{ - let cellModel = createCellModelWith(type: .aimsg) - cellModel.shouldShowLeft = true - let msg = V2NIMMessage() - msg.text = "(Watching her parents toast you respectfully, I feel very uncomfortable. After all, she has been standing on the top of the magic capital since she was a child. She has never seen her parents like this, but should I say that he is really handsome?) Are you?" - cellModel.v2msg = msg - cellModel.timeStamp = TimeInterval(Date().timeStamp - 1000) - array.append(cellModel) - } - do{ - let cellModel = createCellModelWith(type: .text) - cellModel.shouldShowLeft = false - let msg = V2NIMMessage() - msg.text = "who are you" - cellModel.v2msg = msg - cellModel.timeStamp = TimeInterval(Date().timeStamp - 500) - array.append(cellModel) - } - - cellModels = array - - completion() - - } - - -} diff --git a/crush/Crush/Src/Modules/Chat/Util/SessionUtilOC.h b/crush/Crush/Src/Modules/Chat/Util/SessionUtilOC.h deleted file mode 100755 index 8f54a51..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/SessionUtilOC.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// SessionUtilOC.h -// LegendTeam -// -// Created by 梁博 on 20/12/21. -// - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SessionUtilOC : NSObject - -+ (BOOL)shouldReloadWhenInsert:(NSArray *)indexPaths tableView:(UITableView *)tableView; -+ (NSArray *)getIndexPathsWith:(NSArray *)indexs; -+ (CGSize)sizeWithImageOriginSize:(CGSize)originSize - minSize:(CGSize)imageMinSize - maxSize:(CGSize)imageMaxSiz; -/// 时间显示规则 -/// @param msglastTime 时间戳 -/// @param showDetail 是否显示更详细的年月日 ---- 为NO:"今天:3:16 PM、昨天:Yesterday、超过2天:Jun 16, 2021" ----- 为YES:"今天:3:16 PM、昨天:Yesterday 3:16 PM、超过2天:Jun 16, 2021 3:16 PM" -+ (NSString*)showTime:(NSTimeInterval)msglastTime showDetail:(BOOL)showDetail; - - -@end - -NS_ASSUME_NONNULL_END diff --git a/crush/Crush/Src/Modules/Chat/Util/SessionUtilOC.m b/crush/Crush/Src/Modules/Chat/Util/SessionUtilOC.m deleted file mode 100755 index bf470ba..0000000 --- a/crush/Crush/Src/Modules/Chat/Util/SessionUtilOC.m +++ /dev/null @@ -1,204 +0,0 @@ -// -// SessionUtilOC.m -// LegendTeam -// -// Created by 梁博 on 20/12/21. -// - -#import "SessionUtilOC.h" - -@implementation SessionUtilOC - -+ (BOOL)shouldReloadWhenInsert:(NSArray *)indexPaths tableView:(UITableView *)tableView { - // 如果插入数据后,中间有空档,则不能直接插入,需要全量重新加载 - NSMutableDictionary * sectionCurrentCount = [NSMutableDictionary dictionary]; - NSMutableDictionary * sectionMaxCount = [NSMutableDictionary dictionary]; - NSMutableDictionary * sectionInsertingCount = [NSMutableDictionary dictionary]; - - for(NSIndexPath * indexPath in indexPaths) - { - NSInteger section = indexPath.section; - NSInteger count = [tableView numberOfRowsInSection:section]; - sectionCurrentCount[@(section)] = @(count); - } - - for(NSIndexPath * indexPath in indexPaths) - { - NSInteger section = indexPath.section; - NSInteger row = indexPath.row; - NSInteger count = [sectionCurrentCount[@(section)] integerValue]; - NSInteger sectionMaxNum = [sectionMaxCount[@(section)] integerValue]; - NSInteger max = 0; - if (row <= count) - { - sectionCurrentCount[@(section)] = @(count+1); - max = count + 1; - } - else - { - max = row + 1; - } - max = MAX(max, sectionMaxNum); - sectionMaxCount[@(section)] = @(max); - - NSInteger sectionCurrentCount = [sectionInsertingCount[@(section)] integerValue]; - sectionInsertingCount[@(section)] = @(++ sectionCurrentCount); - } - - for(NSNumber * sectionKey in sectionMaxCount.allKeys) - { - NSInteger maxCount = [sectionMaxCount[sectionKey] integerValue]; - NSInteger currentCount = [sectionInsertingCount[sectionKey] integerValue]; - NSInteger section = [sectionKey integerValue]; - NSInteger count = [tableView numberOfRowsInSection:section]; - if (maxCount > count + currentCount) - { - return YES; - } - } - - return NO; -} - - -+ (NSArray *)getIndexPathsWith:(NSArray *)indexs { - NSMutableArray *addIndexPathes = [NSMutableArray array]; - [indexs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[obj integerValue] inSection:0]; - [addIndexPathes addObject:indexPath]; - }]; - return [addIndexPathes copy]; -} - -+ (CGSize)sizeWithImageOriginSize:(CGSize)originSize - minSize:(CGSize)imageMinSize - maxSize:(CGSize)imageMaxSiz{ - - NSInteger imageWidth = originSize.width ,imageHeight = originSize.height; - imageWidth = imageWidth == 0 ? 1 : imageWidth; - imageHeight = imageHeight == 0 ? 1 : imageHeight; - NSInteger imageMinWidth = imageMinSize.width, imageMinHeight = imageMinSize.height; - NSInteger imageMaxWidth = imageMaxSiz.width, imageMaxHeight = imageMaxSiz.height; - - CGFloat newWidth = imageWidth; - CGFloat newHeight = imageHeight; - - if (imageWidth > imageHeight) //宽图 - { - if (imageWidth > imageMaxWidth) { - // 宽度大于最大宽度时, 取最大宽度 100 * 50 60 - newWidth = imageMaxWidth; - // 根据宽度比例获取高度 - newHeight = imageHeight * imageMaxWidth / imageWidth; - } else if (imageWidth < imageMinWidth) { - // 宽度小于于最小宽度时, 取最小宽度 20 * 10 30 - newWidth = imageMinWidth; - // 根据宽度比例获取高度 - newHeight = imageHeight * imageMinWidth / imageWidth; - } - } - else if(imageWidth < imageHeight)//高图 - { - if (imageHeight > imageMaxHeight) { - // 高度大于最大高度时, 取最大高度 50 * 100 60 - newHeight = imageMaxHeight; - newWidth = imageWidth * imageMaxHeight / imageHeight; - } else if (imageHeight < imageMinHeight) { - // 高度小于最小高度时, 取最大高度 10 * 20 30 - newHeight = imageMinHeight; - newWidth = imageWidth * imageMinHeight / imageHeight; - } - } - - - // 控制宽度最大最小显示逻辑 - if (newWidth > imageMaxWidth) { - newWidth = imageMaxWidth; - } - if (newWidth < imageMinWidth) { - newWidth = imageMinWidth; - } - - // 控制高度最大最小显示逻辑 - if (newHeight > imageMaxHeight) { - newHeight = imageMaxHeight; - } - if (newHeight < imageMinHeight) { - newHeight = imageMinHeight; - } - - return CGSizeMake(newWidth, newHeight); -} - - - -+ (NSString*)showTime:(NSTimeInterval)msglastTime showDetail:(BOOL)showDetail { - //今天的时间 - NSDate *nowDate = [NSDate date]; - NSDate *msgDate = [NSDate dateWithTimeIntervalSince1970:msglastTime]; - NSTimeInterval nowTimeInterval = [nowDate timeIntervalSince1970]; - - NSString *result = nil; - NSCalendarUnit components = (NSCalendarUnit)(NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitWeekday|NSCalendarUnitHour | NSCalendarUnitMinute); - NSDateComponents *nowDateComponents = [[NSCalendar currentCalendar] components:components fromDate:nowDate]; - NSDateComponents *msgDateComponents = [[NSCalendar currentCalendar] components:components fromDate:msgDate]; - - double OnedayTimeIntervalValue = 24*60*60; //一天的秒数 - // NSInteger hour = msgDateComponents.hour; -// if (hour > 12) -// { -// hour = hour - 12; -// } - BOOL isSameYer = nowDateComponents.year == msgDateComponents.year; - BOOL isSameMonth = isSameYer && (nowDateComponents.month == msgDateComponents.month); - BOOL isSameDay = isSameYer && isSameMonth && (nowDateComponents.day == msgDateComponents.day); - // 如果要求强制显示AM、需要通过日历控件自己计算 - NSInteger reduceInterval = nowTimeInterval - msglastTime; - if (showDetail) { - if (reduceInterval < OnedayTimeIntervalValue) { - // 24小时内 显示 XX以前 - result =[NSString stringWithFormat:@"%@", [self stringFromDate:msgDate format:@"h:mm aa"]]; - } else if (isSameMonth && (nowDateComponents.day == (msgDateComponents.day + 1))) { - // 昨天 - result =[NSString stringWithFormat:@"Yesterday %@", [self stringFromDate:msgDate format:@"h:mm aa"]]; - } else if (isSameYer) { - // 当年内 - result =[NSString stringWithFormat:@"%@", [self stringFromDate:msgDate format:@"MMM d, h:mm aa"]]; - } else { - result =[NSString stringWithFormat:@"%@", [self stringFromDate:msgDate format:@"MMM d, yyyy h:mm aa"]]; - } - } else { - if (isSameDay) { - // 今天 - result =[NSString stringWithFormat:@"%@", [self stringFromDate:msgDate format:@"h:mm aa"]]; - } else if (isSameMonth && (nowDateComponents.day == (msgDateComponents.day + 1))) { - // 昨天 - result =[NSString stringWithFormat:@"Yesterday"]; - } else if (isSameYer) { - // 当年内 - result =[NSString stringWithFormat:@"%@", [self stringFromDate:msgDate format:@"MMM d"]]; - } else { - result =[NSString stringWithFormat:@"%@", [self stringFromDate:msgDate format:@"MMM d, yyyy"]]; - } - } - - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - [formatter setDateFormat:@"hh:mm aa"]; - NSDate *tempDate = [formatter dateFromString:@"16:20"]; - // 24小时制,需要处理一下显示 - if (tempDate && ([result containsString:@"AM"] || [result containsString:@"PM"])) { - NSString *string1 = [result stringByReplacingOccurrencesOfString:@"AM" withString:@""]; - NSString *string2 = [string1 stringByReplacingOccurrencesOfString:@"PM" withString:@""]; - result = string2; - } - return result; -} - -+ (NSString *)stringFromDate:(NSDate *)date format:(NSString *)format { - NSDateFormatter *fomatter = [[NSDateFormatter alloc] init]; - [fomatter setTimeZone:[NSTimeZone localTimeZone]]; - [fomatter setDateFormat:format]; - return [fomatter stringFromDate:date]; -} - -@end diff --git a/crush/Crush/Src/Modules/Discover/BannerAd/DailyFreeCoinCheckController.swift b/crush/Crush/Src/Modules/Discover/BannerAd/DailyFreeCoinCheckController.swift deleted file mode 100644 index 33cbc22..0000000 --- a/crush/Crush/Src/Modules/Discover/BannerAd/DailyFreeCoinCheckController.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// DailyFreeCoinCheckController.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -import UIKit - -class DailyFreeCoinCheckController: CLBaseViewController { - var container: LTScrollContainer! - - var header: DialyCheckHeaderView! - var introduction: DialyCheckIntroductionView! - var checkDaysView:DailyCheckDaysControl! - var descLabel : LineSpaceLabel! - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - let title = "Dialy Free Crush Coin" - navigationView.alpha0Title = title - navigationView.bgView.alpha = 0 - - container = { - let v = LTScrollContainer() - v.scrollView.delegate = self - v.scrollView.bounces = false - //view.addSubview(v) - v.stackEdge = UIEdgeInsets(top: 0, left: 0, bottom: 24+UIWindow.safeAreaBottom, right: 0) - view.insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - - header = { - let v = DialyCheckHeaderView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - container.stack.setCustomSpacing(24, after: header) - - introduction = { - let v = DialyCheckIntroductionView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - container.stack.setCustomSpacing(16, after: introduction) - - checkDaysView = { - let v = DailyCheckDaysControl() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - container.stack.setCustomSpacing(16, after: checkDaysView) - - descLabel = { - let v = LineSpaceLabel() - v.textColor = .desc - let typo = CLSystemToken.typography(token:.tbs) - v.config(typo) - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - descLabel.text = "Diamonds can be used to pay for chats, as well as unlock other props.\nAfter the sign-off is discontinued, you will re-check in from the first day." - } - - private func setupDatas() { - loadCheckList() - checkToday() - } - - private func loadCheckList(){ - DiscoverProvider.request(.days7CheckList, modelType: Day7CheckList.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - self?.checkDaysView.config(model) - self?.introduction.config(model) - case .failure: - break - } - } - } - - private func checkToday(){ - Hud.showIndicator() - DiscoverProvider.request(.dailyCheck, modelType: EmptyModel.self) {[weak self] result in - - switch result { - case .success: - self?.loadCheckList() - Hud.toast(str: "今日已签到") - case .failure (let failure): - Hud.hideIndicator() - switch failure { - case let .serviceError(code, _): - if code == .sign_usernotLoggedIn{ - // ... - self?.close() - } - default: - break - } - } - } - } - - private func setupEvents() { - } -} - -extension DailyFreeCoinCheckController: UIScrollViewDelegate{ - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviViewsAlpha(scrollView: scrollView, alphaViews: [navigationView.titleLabel, navigationView.bgView], oppositeViews: []) - } -} diff --git a/crush/Crush/Src/Modules/Discover/BannerAd/View/DailyFreenCoinCheckPieces.swift b/crush/Crush/Src/Modules/Discover/BannerAd/View/DailyFreenCoinCheckPieces.swift deleted file mode 100644 index b6f89c0..0000000 --- a/crush/Crush/Src/Modules/Discover/BannerAd/View/DailyFreenCoinCheckPieces.swift +++ /dev/null @@ -1,452 +0,0 @@ -// -// DailyFreenCoinCheckPieces.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -class DialyCheckHeaderView: UIView { - var imageView: UIImageView! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - // backgroundColor = .random - - imageView = { - let v = UIImageView() - v.image = UIImage(named: "daily_check_bg_banner") - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(248 / 393.0) - } - return v - }() - } -} - -class DialyCheckIntroductionView: UIView { - var label: LineSpaceLabel! - var lineIv: UIImageView! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { -// backgroundColor = .random - - label = { - let v = LineSpaceLabel() - v.textColor = .text - v.textAlignment = .center - let typo = CLSystemToken.typography(token: .ttm) - v.config(typo) - - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - // make.bottom.equalToSuperview().offset(-40) - } - return v - }() - - lineIv = { - let v = UIImageView() - v.image = UIImage(named: "daily_check_line") - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview() - make.top.equalTo(label.snp.bottom).offset(0) - make.bottom.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(88 / 345.0) - } - return v - }() - - label.text = "You have checked in for 1 consecutive days" - } - - func config(_ checkList: Day7CheckList?) { - guard let checkData = checkList, let days = checkData.continuousDays else { return } - - label.text = "You have checked in for \(days) consecutive days" - } -} - -class DailyCheckDaysControl: UIView { - var stackV: UIStackView! - var row1: UIStackView! - var row2: UIStackView! - - // row 1 - var day1: DailyCheckDayView! - var day2: DailyCheckDayView! - var day3: DailyCheckDayView! - - // row 2 - var day4: DailyCheckDayView! - var day5: DailyCheckDayView! - var day6: DailyCheckDayView! - - var day7: DailyCheckDay7View! - - var daysItems = [DailyCheckDayView]() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - stackV = { - let v = UIStackView() - v.spacing = 12 - v.axis = .vertical - // v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - row1 = { - let v = UIStackView() - v.spacing = 8 - v.distribution = .fillEqually - v.alignment = .center - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(120) - } - return v - }() - - row2 = { - let v = UIStackView() - v.spacing = 8 - v.distribution = .fillEqually - v.alignment = .center - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(120) - } - return v - }() - - day1 = { - let v = DailyCheckDayView() - row1.addArrangedSubview(v) - return v - }() - - day2 = { - let v = DailyCheckDayView() - row1.addArrangedSubview(v) - return v - }() - - day3 = { - let v = DailyCheckDayView() - row1.addArrangedSubview(v) - return v - }() - - day4 = { - let v = DailyCheckDayView() - row2.addArrangedSubview(v) - return v - }() - - day5 = { - let v = DailyCheckDayView() - row2.addArrangedSubview(v) - return v - }() - - day6 = { - let v = DailyCheckDayView() - row2.addArrangedSubview(v) - return v - }() - - day7 = { - let v = DailyCheckDay7View() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(120) - } - return v - }() - - daysItems.append(day1) - daysItems.append(day2) - daysItems.append(day3) - daysItems.append(day4) - daysItems.append(day5) - daysItems.append(day6) - daysItems.append(day7) - } - - private func setupData() { - day1.setupDay(1) - day2.setupDay(2) - day3.setupDay(3) - day4.setupDay(4) - day5.setupDay(5) - day6.setupDay(6) - day7.setupDay(7) - } - - // MARK: Public - - func config(_ checkList: Day7CheckList?) { - guard let checkData = checkList, let daysList = checkData.list else { return } - - for (index, per) in daysList.enumerated() { - let dayItem = daysItems[index] - dayItem.setupCoin(per.coinNum ?? 0) - dayItem.setupCheckedTheDay(per.signIn.boolValue) - } - } - -// func setupCheckedTheDay(_ checked: Bool) { -// day1.setupCheckedTheDay(checked) -// day2.setupCheckedTheDay(checked) -// day3.setupCheckedTheDay(false) -// day4.setupCheckedTheDay(false) -// day5.setupCheckedTheDay(false) -// day6.setupCheckedTheDay(false) -// day7.setupCheckedTheDay(false) -// } -} - -class DailyCheckDayView: UIView { - /// 未签到时,会亮起的主题色背景 - var gradidentBg: GradientView! - var imageViewBg: UIImageView! - - var dayFlagContainer: UIView! - var dayLabel: CLLabel! - - var coinAndLabel: CLIconLabel! - - var checkedStackH: UIStackView! - //var checkButton: StyleButton! - var notStartLabel : CLLabel! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupViews() { - cornerRadius = 16 - layer.borderColor = UIColor.c.con.cgColor - layer.borderWidth = 1 - - gradidentBg = { - let gradient = CLSystemToken.gradient(token: .cpgn) - let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - imageViewBg = { - let v = UIImageView() - v.image = UIImage(named: "daily_check_bg_diamond") - addSubview(v) - v.snp.makeConstraints { make in - // make.edges.equalToSuperview() - make.trailing.top.bottom.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(120 / 140.0) - } - return v - }() - - dayFlagContainer = { - let v = UIView() - v.backgroundColor = .c.cseln - v.layer.cornerRadius = 16 - v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner] - addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.equalToSuperview() - make.height.equalTo(28) - } - return v - }() - - dayLabel = { - let v = CLLabel() - v.font = .t.tls - dayFlagContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - - coinAndLabel = { - let v = CLIconLabel() - v.iconImageView.image = UIImage(named: "icon_32_diamond") - v.iconSize = CGSizeMake(24, 24) - v.contentLabel.font = .t.tndm - v.contentLabel.text = "-" - addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - checkedStackH = { - let v = UIStackView() - v.spacing = 4 - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(20) - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-10) - } - - let icon = UIImageView() - icon.image = UIImage(named: "radio_checkbox_unselected") - icon.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 20, height: 20)) - } - v.addArrangedSubview(icon) - - let label = CLLabel() - label.font = .t.tls - label.text = "Checked" - v.addArrangedSubview(label) - v.isHidden = true - return v - }() - -// checkButton = { -// let v = StyleButton() -// v.contrastTertiaryDark(size: .small) -// v.setTitle("Check in", for: .normal) -// addSubview(v) -// v.snp.makeConstraints { make in -// make.centerX.equalToSuperview() -// make.bottom.equalToSuperview().offset(-4) -// } -// return v -// }() - - notStartLabel = { - let v = CLLabel() - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-10) - } - return v - }() - -// #warning("test") -// testData() - - notStartLabel.text = "Not Started" - } - - private func testData() { - setupDay(1) - coinAndLabel.contentLabel.text = "5" - setupCheckedTheDay(true) - } - - // MARK: - Public - func setupCoin(_ coin: Int){ - let coin = Coin(cents: coin) - coinAndLabel.contentLabel.text = coin.formatted - } - - func setupCheckedTheDay(_ checked: Bool) { - if checked { - layer.borderColor = UIColor.c.con.cgColor - gradidentBg.isHidden = true - checkedStackH.isHidden = false - notStartLabel.isHidden = true - } else { - layer.borderColor = CLGlobalToken.color(token: .glo_color_orange_10)?.cgColor - gradidentBg.isHidden = false - checkedStackH.isHidden = true - notStartLabel.isHidden = false - } - } - - func setupDay(_ whichDay: Int) { - dayLabel.text = "Day \(whichDay)" - } -} - -class DailyCheckDay7View: DailyCheckDayView { - override init(frame: CGRect) { - super.init(frame: frame) - // setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func setupViews() { - super.setupViews() - - imageViewBg.snp.remakeConstraints { make in - make.leading.equalToSuperview().offset(92) - make.top.bottom.equalToSuperview() - make.height.equalTo(imageViewBg.snp.width).multipliedBy(120 / 140.0) - } - - coinAndLabel.snp.remakeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(imageViewBg) - } - - notStartLabel.snp.remakeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-28) - } - - checkedStackH.snp.remakeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - } -} diff --git a/crush/Crush/Src/Modules/Discover/DiscoverRolesGridController.swift b/crush/Crush/Src/Modules/Discover/DiscoverRolesGridController.swift deleted file mode 100644 index 352efde..0000000 --- a/crush/Crush/Src/Modules/Discover/DiscoverRolesGridController.swift +++ /dev/null @@ -1,237 +0,0 @@ -// -// DiscoverRolesGridController.swift -// Crush -// -// Created by Leon on 2025/9/8. -// - -import JXPagingView -import UIKit - -class DiscoverRolesGridController: CLBaseGridController { - var viewModel: DiscoverHomeRolesViewModel! - - var tagsChooseView = HorizontalScrollTagsView() - - var selectTagCodes = [String]() - var allCatogryTab: Bool = false - - // 分页加载过的aiId,第一页请求时清空 - var loadedAiIds = [Int]() - - // Flag - var refreshDatasWhenAppear:Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if refreshDatasWhenAppear{ - collectionView.mj_header?.beginRefreshing() - refreshDatasWhenAppear = false - } - } - - private func setupViews() { - - if allCatogryTab{ - collectionView.snp.remakeConstraints { make in - make.edges.equalToSuperview() - } - }else{ - view.addSubview(tagsChooseView) - tagsChooseView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(56) - } - - collectionView.snp.remakeConstraints { make in - make.top.equalTo(tagsChooseView.snp.bottom) - make.leading.trailing.bottom.equalToSuperview() - } - } - - navigationView.isHidden = true - collectionView.register(MeRootPageRollCell.self, forCellWithReuseIdentifier: "MeRootPageRollCell") - collectionView.register(UICollectionReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: "UICollectionReusableView") - - let lr = CGFloat.lrs - let itemW = (UIScreen.width - lr * 2 - 16) * 0.5 - let itemH = itemW * 260 / 165.0 + 112 - - layout.scrollDirection = .vertical - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = .init(top: 12, left: lr, bottom: 24 + UIWindow.safeAreaBottom, right: lr) - layout.itemSize = .init(width: itemW, height: itemH) - layout.invalidateLayout() - - - - addRefreshHeaderFooter() - } - - private func setupDatas() { - // collectionView.mj_header?.beginRefreshing() - loadNewData() - - if allCatogryTab == false{ - var tagsName = [String]() - for per in viewModel.tags ?? [] { - let formatString = "#\(per.name)" - tagsName.append(formatString) - } - - tagsChooseView.setTags(tagsName) - } - } - - override func loadData() { - - if page == 1{ - loadedAiIds.removeAll() - } - - viewModel.loadRoles(pn: page, codes: selectTagCodes, exList: loadedAiIds) { [weak self] status, roles in - self?.collectionView.mj_header?.endRefreshing() - self?.collectionView.mj_footer?.endRefreshing() - if status { - let array = roles ?? [] - - for per in array { - if let theId = per.aiId{ - self?.loadedAiIds.append(theId) - } - } - - if self?.page == 1 { - self?.collectionView.contentOffset = .zero - self?.datas = array - self?.collectionView.mj_footer?.resetNoMoreData() - // self?.view.setupEmpty(empty: array.count <= 0, msg: "暂无角色") - if array.count > 0 { - self?.view.hideEmpty() - } else { - self?.view.showStartYEmpty(text: "暂无角色", startY: 96) - } - } else { - self?.datas.append(contentsOf: array) - if array.count <= 0 { - self?.collectionView.mj_footer?.endRefreshingWithNoMoreData() - } - } - self?.collectionView.reloadData() - } - } - } - - private func setupEvents() { - - tagsChooseView.selectionCallback = {[weak self] selectedTags, selectedIndices in - guard let `self` = self else { - return - } - print("选中的标签: \(selectedTags)") - print("选中的索引: \(selectedIndices)") - - self.selectTagCodes.removeAll() - for (index, tag) in (self.viewModel.tags ?? []).enumerated() { - if selectedIndices.contains(index) { - if let code = tag.code { - self.selectTagCodes.append(code) - } - } - } - self.loadNewData() - } - } - - // MARK: - Public - -// func tryLoad(code: String) { -// if let tagCode = viewModel.selectTagCode, code == tagCode { -// return -// } -// viewModel.selectTagCode = code -// loadNewData() -// } - - // MARK: - UICollectionView - - override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MeRootPageRollCell", for: indexPath) as! MeRootPageRollCell - cell.cellType = .discoverList - if let data = datas[indexPath.item] as? AIRoleInfo { - cell.config(data: data) - } - return cell - } - - override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - if let data = datas[indexPath.item] as? AIRoleInfo { - AppRouter.goChatVC(aiId: data.aiId) - } - } - -// func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { -// if kind == UICollectionView.elementKindSectionHeader { -// let header = collectionView.dequeueReusableSupplementaryView( -// ofKind: kind, -// withReuseIdentifier: "UICollectionReusableView", -// for: indexPath -// ) -// -// if !allCatogryTab{ -// if tagsChooseView.superview == nil || tagsChooseView.superview != header { -// tagsChooseView.removeFromSuperview() -// } -// -// header.addSubview(tagsChooseView) -// tagsChooseView.snp.makeConstraints { make in -// make.top.leading.trailing.equalToSuperview() -// make.bottom.equalToSuperview() -// } -// } -// -// return header -// } -// return UICollectionReusableView() -// } -// -// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { -// let height = allCatogryTab ? 0.0 : 56.0 -// return CGSize(width: UIScreen.width, height: height) -// } -} - -extension DiscoverRolesGridController { - -} - -extension DiscoverRolesGridController: JXPagingViewListViewDelegate { - func listView() -> UIView { - return view - } - - func listScrollView() -> UIScrollView { - return collectionView - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - listViewDidScrollCallback = callback - } -} diff --git a/crush/Crush/Src/Modules/Discover/DiscoverRootPageController.swift b/crush/Crush/Src/Modules/Discover/DiscoverRootPageController.swift deleted file mode 100644 index 7a2c960..0000000 --- a/crush/Crush/Src/Modules/Discover/DiscoverRootPageController.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// DiscoverRootPageController.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit -import Combine -class DiscoverRootPageController: CLTabRootController { - - @Published var filterModel = RolesFilterModel() - private var cancellables = Set() - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - navigationView.bgView.alpha = 0 - navigationView.setupBgViewToStatusBarHeight() - navigationView.isUserInteractionEnabled = false - } - - private func setupDatas() { - loadDiscoverInfo() - } - - private func loadDiscoverInfo() { - DiscoverProvider.request(.discoverInfo, modelType: DiscoverHomeInfo.self) { [weak self] result in - switch result { - case let .success(model): -// dlog(model) - self?.container.headerView.leaderboardEntryView.config(model) - case .failure: - break - } - } - } - - private func setupEvents() { - container.pinHeaderView.filterButton.addTarget(self, action: #selector(tapFilterButton(button:)), for: .touchUpInside) - container.headerView.banner.topButton.addTarget(self, action: #selector(tapBannerAd), for: .touchUpInside) - - $filterModel.sink {[weak self] model in - self?.container.pinHeaderView.setupFilterCount(model.filterCount()) - }.store(in: &cancellables) - } - - // MARK: Action - - @objc private func tapFilterButton(button: UIButton) { - let vc = RolesFilterController() - vc.filterType = .discover - vc.rolesFilterModel = filterModel - presentNaviRootVc(vc: vc) - vc.completion = {[weak self] model in - self?.filterModel = model - // dlog("完成fitler选择:\(model)") - self?.container.setupFilter(filter: model) - } - } - - @objc private func tapBannerAd() { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{ - return - } - let vc = DailyFreeCoinCheckController() - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/crush/Crush/Src/Modules/Discover/Filter/RolesFilterController.swift b/crush/Crush/Src/Modules/Discover/Filter/RolesFilterController.swift deleted file mode 100644 index bd82eda..0000000 --- a/crush/Crush/Src/Modules/Discover/Filter/RolesFilterController.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// RolesFilterController.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -import UIKit - -enum RolesFilterType { - case meet - case discover -} - -class RolesFilterController: CLBaseViewController { - let startOfGenderIndex = 5000 - let startOfAgeIndex = 5100 - - var operateButton: StyleButton! - var container: LTScrollContainer! - - var titleView: TitleView! - - var genderView: FlowAutoLayoutContainer! - var ageView: FlowAutoLayoutContainer! - - var typeView: RoleFilterTypeView! - - var ageTypes = [AIRoleListRequestAge.AGE_1, - AIRoleListRequestAge.AGE_2, - AIRoleListRequestAge.AGE_3, - AIRoleListRequestAge.AGE_4, - AIRoleListRequestAge.AGE_5, - ] - - var filterType: RolesFilterType = .discover - - var rolesFilterModel : RolesFilterModel! - - var completion: ((_ rolesFilterModel: RolesFilterModel) -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - let title = "Filter" - navigationView.alpha0Title = title - navigationView.setupBackButtonCloseIcon() - - operateButton = { - let v = StyleButton() - v.primary(size: .large) - v.setTitle("Complete", for: .normal) - v.addTarget(self, action: #selector(operateButtonTapped), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom * 0.5) - } - return v - }() - - container = { - let v = LTScrollContainer() - v.stack.spacing = 32 - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - make.bottom.equalTo(operateButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.title = title - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - container.stack.setCustomSpacing(12, after: titleView) - - do { - let perContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - let perTitleLabel = { - let v = CLLabel() - v.font = .t.ttm - perContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - } - return v - }() - perTitleLabel.text = "Gender" - - genderView = { - let v = FlowAutoLayoutContainer() - perContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(perTitleLabel.snp.bottom).offset(16) - make.bottom.equalToSuperview() - } - return v - }() - } - - do { - let perContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - let perTitleLabel = { - let v = CLLabel() - v.font = .t.ttm - perContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - } - return v - }() - perTitleLabel.text = "Age" - - ageView = { - let v = FlowAutoLayoutContainer() - perContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(perTitleLabel.snp.bottom).offset(16) - make.bottom.equalToSuperview() - } - return v - }() - } - - typeView = { - let v = RoleFilterTypeView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - } - - private func setupDatas() { - do { - let items = [Sex.male, Sex.female, Sex.noncomfirming] - for (index, per) in items.enumerated() { - let button = createChipButtonBy(name: per.localizedText) - button.tag = startOfGenderIndex + index - - if let sexsSelect = rolesFilterModel.sex{ - if sexsSelect.contains(per){ - button.isSelected = true - } - } - -// if let selectedSex = rolesFilterModel.sex, per == selectedSex { -// button.isSelected = true -// } - genderView.addArrangedSubview(button) - } - } - - do { - var titles = [String]() - for per in ageTypes { - if let string = per.localized { - titles.append(string) - } - } - for (index, per) in ageTypes.enumerated() { - let button = createChipButtonBy(name: per.localized!) - button.tag = startOfAgeIndex + index - if let selecteAge = rolesFilterModel.age, selecteAge == per { - button.isSelected = true - } - ageView.addArrangedSubview(button) - } - } - - typeView.selectTypes(rolesFilterModel.roleCodeList) - } - - private func setupEvents() { - } - - // MARK: - Helper - - private func createChipButtonBy(name: String) -> EPChipFilterButton { - let button = EPChipFilterButton() - button.text = name - button.addTarget(self, action: #selector(tapChipButton(sender:)), for: .touchUpInside) - return button - } - - // MARK: - Action - - @objc private func tapChipButton(sender: UIButton) { - guard let superView: FlowAutoLayoutContainer = sender.superview as? FlowAutoLayoutContainer else { - return - } - - // 根据filterType决定单选还是多选 - if sender.isSelected { - sender.isSelected = false - } else { - if filterType == .meet || filterType == .discover { // 单选模式看下面: - for per in superView.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - } - // discover模式:多选,不需要清除其他选择 - sender.isSelected = true - } - } - - @objc private func operateButtonTapped() { - var confirmFilterModel = RolesFilterModel() - - // 获取选中的性别 - if filterType == .discover { - // discover模式:多选 - var selectedSexes: [Sex] = [] - for subview in genderView.subviews { - if let button = subview as? EPChipFilterButton, button.isSelected { - let index = button.tag - startOfGenderIndex - if index >= 0 && index < 3, let sex = Sex(rawValue: index) { - selectedSexes.append(sex) - } - } - } - if !selectedSexes.isEmpty { - confirmFilterModel.sex = selectedSexes - } - } else { - // meet模式:单选 - var selectedSex: Sex? - for subview in genderView.subviews { - if let button = subview as? EPChipFilterButton, button.isSelected { - let index = button.tag - startOfGenderIndex - if index >= 0 && index < 3 { - selectedSex = Sex(rawValue: index) - } - break - } - } - if let selectedSex = selectedSex { - confirmFilterModel.sex = [selectedSex] - } - } - - // 获取选中的年龄 - var selectedAge: AIRoleListRequestAge? - for subview in ageView.subviews { - if let button = subview as? EPChipFilterButton, button.isSelected { - let index = button.tag - startOfAgeIndex - if index >= 0 && index < ageTypes.count { - selectedAge = ageTypes[index] - } - break - } - } - - // 获取选中的角色类型 - let selectedRoleTypes = typeView.getSelectedTypes() - - // 更新过滤模型 - if selectedAge != nil { - confirmFilterModel.age = selectedAge - } - - if selectedRoleTypes.count > 0 { - confirmFilterModel.roleCodeList = selectedRoleTypes - }else { - confirmFilterModel.roleCodeList = nil - } - - completion?(confirmFilterModel) - - // 关闭当前页面 - dismiss(animated: true) - } -} diff --git a/crush/Crush/Src/Modules/Discover/Filter/View/RoleFilterPiecesView.swift b/crush/Crush/Src/Modules/Discover/Filter/View/RoleFilterPiecesView.swift deleted file mode 100644 index 83745bb..0000000 --- a/crush/Crush/Src/Modules/Discover/Filter/View/RoleFilterPiecesView.swift +++ /dev/null @@ -1,302 +0,0 @@ -// -// RoleFilterPieces.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -enum RoleFilterTypeItemType:String { - case original = "R00002" - case anime = "R00004" - case game = "R00005" - case filmAndTv = "R00006" -} - -class RoleFilterTypeItem: UIView { - var gradientBg: GradientView! - var titleLabel: CLLabel! - - var figureImageView: CLImageView! - - var topButton: UIButton! - - var type: RoleFilterTypeItemType! = .original - var selected : Bool = false { - didSet{ - setupSelected(selected) - } - } - - override private init(frame: CGRect) { - super.init(frame: frame) - } - - convenience init(type: RoleFilterTypeItemType) { - self.init(frame: .zero) - self.type = type - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csen - cornerRadius = 16 - layer.borderWidth = 1 - layer.borderColor = UIColor.c.con.cgColor - - var colors = [UIColor]() - switch type { - case .original: - colors = [UIColor(hex: "#62E1F2"), UIColor(hex: "#6296F2")] - case .anime: - colors = [UIColor(hex: "#E03CE6").withAlphaComponent(0.5), UIColor(hex: "#E63C8B").withAlphaComponent(0.5)] - case .game: - colors = [UIColor(hex: "#475CFF").withAlphaComponent(0.5), UIColor(hex: "#7B47FF").withAlphaComponent(0.5)] - case .filmAndTv: - colors = [UIColor(hex: "#FF6464").withAlphaComponent(0.5), UIColor(hex: "#FFA264").withAlphaComponent(0.5)] - default: - break - } - - gradientBg = { - let v = GradientView(colors: colors, gradientType: .topToBottom) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - v.isHidden = true - return v - }() - - titleLabel = { - let v = CLLabel() - v.font = .t.tlm - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.leading.equalToSuperview().offset(12) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - - figureImageView = { - let v = CLImageView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(0) // 28 - make.top.equalToSuperview().offset(36) - make.bottom.trailing.equalToSuperview() - } - return v - }() - - topButton = { - let v = UIButton() - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - switch type { - case .original: - titleLabel.text = "Original" - figureImageView.image = UIImage(named: "role_filter_img_oc") - case .anime: - titleLabel.text = "Anime" - figureImageView.image = UIImage(named: "role_filter_img_anime") - case .game: - titleLabel.text = "Game" - figureImageView.image = UIImage(named: "role_filter_img_game") - case .filmAndTv: - titleLabel.text = "Film&TV" - figureImageView.image = UIImage(named: "role_filter_img_filmTV") - default: - break - } - } - - private func setupSelected(_ selected: Bool) { - gradientBg.isHidden = !selected - } -} - -class RoleFilterTypeView: UIView { - var bigTitle: CLLabel! - - var subTitleLabel1: CLLabel! - var stackH1: UIStackView! - - var subTitleLabel2: CLLabel! - var stackH2: UIStackView! - - var item1: RoleFilterTypeItem! - var item2: RoleFilterTypeItem! - var item3: RoleFilterTypeItem! - var item4: RoleFilterTypeItem! - - var items = [RoleFilterTypeItem]() - - var selectType:RoleFilterTypeItemType? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - let itemWidth = floor((UIScreen.width - 24 * 2 - 12 * 2) / 3.0) - let itemHeight = itemWidth - - bigTitle = { - let v = CLLabel() - v.font = .t.ttm - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - } - return v - }() - - subTitleLabel1 = { - let v = CLLabel() - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalTo(bigTitle.snp.bottom).offset(20) - } - return v - }() - - stackH1 = { - let v = UIStackView() - v.distribution = .fillEqually - v.spacing = 12 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(subTitleLabel1.snp.bottom).offset(12) - make.height.equalTo(itemHeight) - } - return v - }() - - item1 = { - let v = RoleFilterTypeItem(type: .original) - stackH1.addArrangedSubview(v) - return v - }() - - stackH1.addArrangedSubview(UIView()) - stackH1.addArrangedSubview(UIView()) - - subTitleLabel2 = { - let v = CLLabel() - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalTo(stackH1.snp.bottom).offset(20) - } - return v - }() - - stackH2 = { - let v = UIStackView() - v.distribution = .fillEqually - v.spacing = 12 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(subTitleLabel2.snp.bottom).offset(12) - make.height.equalTo(itemHeight) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - item2 = { - let v = RoleFilterTypeItem(type: .anime) - stackH2.addArrangedSubview(v) - return v - }() - - item3 = { - let v = RoleFilterTypeItem(type: .game) - stackH2.addArrangedSubview(v) - return v - }() - - item4 = { - let v = RoleFilterTypeItem(type: .filmAndTv) - stackH2.addArrangedSubview(v) - return v - }() - - bigTitle.text = "Type" - #warning("to do, 这里的文案最好用字典中的!") - subTitleLabel1.text = "Original" - subTitleLabel2.text = "Fanfiction" - - items.append(item1) - items.append(item2) - items.append(item3) - items.append(item4) - - item1.topButton.addTarget(self, action: #selector(tapTypeButton(button:)), for: .touchUpInside) - item2.topButton.addTarget(self, action: #selector(tapTypeButton(button:)), for: .touchUpInside) - item3.topButton.addTarget(self, action: #selector(tapTypeButton(button:)), for: .touchUpInside) - item4.topButton.addTarget(self, action: #selector(tapTypeButton(button:)), for: .touchUpInside) - } - - // MARK: - Public - - func selectTypes(_ types: [String]?){ - guard let roleCodes = types else{return} - for per in roleCodes { - if per == RoleFilterTypeItemType.original.rawValue{ - item1.selected = true - }else if per == RoleFilterTypeItemType.anime.rawValue{ - item2.selected = true - }else if per == RoleFilterTypeItemType.game.rawValue{ - item3.selected = true - }else if per == RoleFilterTypeItemType.filmAndTv.rawValue{ - item4.selected = true - } - } - - } - - func getSelectedTypes() -> [String] { - var selectedTypes: [String] = [] - for item in items { - if item.selected { - selectedTypes.append(item.type.rawValue) - } - } - return selectedTypes - } - - // MARK: - Action - - @objc private func tapTypeButton(button:UIButton){ - guard let superView = button.superview else { return } - - let index = items.firstIndex(of: superView as! RoleFilterTypeItem) - if let index = index { - let item = items[index] - item.selected = !item.selected - } - } -} diff --git a/crush/Crush/Src/Modules/Discover/Leaderboard/LeaderboardListController.swift b/crush/Crush/Src/Modules/Discover/Leaderboard/LeaderboardListController.swift deleted file mode 100644 index 62c4652..0000000 --- a/crush/Crush/Src/Modules/Discover/Leaderboard/LeaderboardListController.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// LeaderboardListController.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -import JXPagingView -import UIKit - -class LeaderboardListController: CLBaseTableController { - var type: DiscoverRankType = .chat - - // 拆分后的数据 - private var topThreeData: [AIDiscoverTop] = [] - private var normalListData: [AIDiscoverTop] = [] - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - tableView.register(LeaderboardListCell.self, forCellReuseIdentifier: "LeaderboardListCell") - tableView.register(LeaderboardListTopThreeCell.self, forCellReuseIdentifier: "LeaderboardListTopThreeCell") - - // addRefreshHeaderFooter() - addRefreshHeader() - } - - private func setupDatas() { -// loadData() - tableView.mj_header?.beginRefreshing() - } - - override func loadData() { - var api: DiscoverAPI = .rankChatList - if type == .heartbeat { - api = .rankHeartbeatList - } else if type == .gift { - api = .rankGiftList - } - DiscoverProvider.request(api, modelType: [AIDiscoverTop].self) { [weak self] result in - self?.tableView.mj_header?.endRefreshing() - switch result { - case let .success(model): - self?.splitData(model ?? []) -// #warning("test") -// self?.splitData([]) - self?.tableView.reloadData() - case .failure: - break - } - } - } - - // 拆分数据:前三条给TopThree,剩余给普通列表 - private func splitData(_ data: [AIDiscoverTop]) { - topThreeData = Array(data.prefix(3)) - normalListData = Array(data.dropFirst(3)) - - if data.count > 0{ - view.hideEmpty() - }else{ - view.showEmpty(text: "暂无数据") - } - } - - private func setupEvents() { - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return 2 - } - - // override tableView delegate - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0 { - return topThreeData.isEmpty ? 0 : 1 - } - return normalListData.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - if indexPath.section == 0 { - let cell = tableView.dequeueReusableCell(withIdentifier: "LeaderboardListTopThreeCell", for: indexPath) as! LeaderboardListTopThreeCell - cell.config(topThreeData, type: type) - return cell - } - - let cell = tableView.dequeueReusableCell(withIdentifier: "LeaderboardListCell", for: indexPath) as! LeaderboardListCell - let data = normalListData[indexPath.row] - cell.config(data, type: type) - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.section == 0 { - return - } - - let data = normalListData[indexPath.row] - guard let aiId = data.aiId else { - return - } -// AppRouter.goAIRoleHome(aiId: aiId) - AppRouter.goChatVC(aiId: aiId) - } -} - -extension LeaderboardListController: JXPagingViewListViewDelegate { - func listView() -> UIView { - return view - } - - func listScrollView() -> UIScrollView { - return tableView - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - listViewDidScrollCallback = callback - } -} diff --git a/crush/Crush/Src/Modules/Discover/Leaderboard/LeaderboardPagerMainController.swift b/crush/Crush/Src/Modules/Discover/Leaderboard/LeaderboardPagerMainController.swift deleted file mode 100644 index ab8ee78..0000000 --- a/crush/Crush/Src/Modules/Discover/Leaderboard/LeaderboardPagerMainController.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// LeaderboardPagerMainController.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -import JXPagingView -import JXSegmentedView -import UIKit - -class LeaderboardPagerMainController: CLBaseViewController { - var navBgIv: UIImageView! - var helpGhostButton: EPIconGhostButton! - - var titleView: TitleView! - - private let segmentedViewHeight = 40 - private lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - private lazy var pagingView = JXPagingListRefreshView(delegate: self) - private var controllers = [JXPagingViewListViewDelegate]() - private let dataSource = JXSegmentedTitleDataSource() - - var headerViewHeight = 0// 60 - - private lazy var chatLeaderboardVc = LeaderboardListController() - private lazy var hearbeatLeaderboardVc = LeaderboardListController() - private lazy var giftLeaderboardVc = LeaderboardListController() - - var types = [DiscoverRankType.chat, DiscoverRankType.heartbeat, DiscoverRankType.gift] - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - let title = "Leaderboard" - navigationView.alpha0Title = title - navigationView.bgView.alpha = 0 - - navBgIv = { - let v = UIImageView() - v.image = UIImage(named: "discover_home_bg_rank") - navigationView.insertSubview(v, aboveSubview: navigationView.bgView) - v.snp.makeConstraints { make in - make.leading.top.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(v.image!.size.height/v.image!.size.width) - } - return v - }() - - helpGhostButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .faq) - v.addTarget(self, action: #selector(tapHelpButton), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - navigationView.paddingRightForRightStack = 16 - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerTopPadding = 16 - v.title = title - return v - }() - titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: titleView.preCalculateHeight()) - headerViewHeight = Int(titleView.preCalculateHeight()) -// dlog("calclulate: \(headerViewHeight)") - - pagingView.mainTableView.backgroundColor = UIColor.clear - pagingView.mainTableView.contentInsetAdjustmentBehavior = .never - pagingView.mainTableView.sectionHeaderHeight = 0 - pagingView.mainTableView.sectionFooterHeight = 0 - pagingView.mainTableView.estimatedSectionHeaderHeight = 0 - pagingView.mainTableView.estimatedSectionFooterHeight = 0 - pagingView.mainTableView.estimatedRowHeight = 0 - - if #available(iOS 15.0, *) { - pagingView.mainTableView.sectionHeaderTopPadding = 0 - } else { - // Fallback on earlier versions - } - - pagingView.mainTableView.gestureDelegate = self - view.addSubview(pagingView) - pagingView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - //make.top.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - - dataSource.clNormalStyle() - var titles = [String]() - for per in types { - titles.append(per.localizedText) - } - dataSource.titles = titles - - segmentedView.listContainer = pagingView.listContainerView - segmentedView.dataSource = dataSource - segmentedView.delegate = self - segmentedView.clNormalStyle() - - chatLeaderboardVc.type = .chat - hearbeatLeaderboardVc.type = .heartbeat - giftLeaderboardVc.type = .gift - } - - private func setupDatas() { - } - - private func setupEvents() { - } - - // MARK: Action - @objc private func tapHelpButton(){ - let content = "热聊榜以AI聊天会话数高低排名\n心动榜以AI角色所有对话者产生的心动值之和的高低排名\n礼物榜以AI角色所收到礼物打赏价值之和排名" - let alert = Alert(title: "Tips", text: content) - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) { - } - alert.addAction(action1) - alert.show() - } -} - -extension LeaderboardPagerMainController: JXSegmentedViewDelegate, JXPagingViewDelegate { - func tableHeaderViewHeight(in _: JXPagingView) -> Int { - return Int(headerViewHeight) - } - - func tableHeaderView(in _: JXPagingView) -> UIView { - return titleView - } - - func heightForPinSectionHeader(in _: JXPagingView) -> Int { - return segmentedViewHeight - } - - func viewForPinSectionHeader(in _: JXPagingView) -> UIView { - return segmentedView - } - - func numberOfLists(in _: JXPagingView) -> Int { - return dataSource.titles.count - } - - func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { - if index == 0 { - return chatLeaderboardVc - } else if index == 1 { - return hearbeatLeaderboardVc - } else { - return giftLeaderboardVc - } - } - - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { - } - - func mainTableViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviViewsAlpha(scrollView: scrollView, alphaViews: [navigationView.titleLabel, navigationView.bgView], oppositeViews: [navBgIv]) - } -} - -extension LeaderboardPagerMainController: JXPagingMainTableViewGestureDelegate { - func mainTableViewGestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - // 如果是 UICollectionView 的手势,先判断滚动方向 - if let panGesture = otherGestureRecognizer as? UIPanGestureRecognizer, - let otherView = otherGestureRecognizer.view, - otherView is UICollectionView { - let velocity = panGesture.velocity(in: otherView) - // 横向滚动时禁止 - if abs(velocity.x) > abs(velocity.y) { - return false - } - } - return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - } -} diff --git a/crush/Crush/Src/Modules/Discover/Leaderboard/View/LeaderboardListCell.swift b/crush/Crush/Src/Modules/Discover/Leaderboard/View/LeaderboardListCell.swift deleted file mode 100644 index 39791c9..0000000 --- a/crush/Crush/Src/Modules/Discover/Leaderboard/View/LeaderboardListCell.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// LeaderboardListCell.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -class LeaderboardListCell: UITableViewCell { - var avatarView: CLImageView! - var iconAndLabel: CLIconLabel! - var rankLabel: UILabel! - var contentStackView: UIStackView! - var nameLabel: CLLabel! -// var descLabel: CLLabel! - var descIconAndLabel: CLIconLabel! - - var data: AIDiscoverTop? - var type: DiscoverRankType = .chat - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - avatarView = { - let v = CLImageView() - v.contentMode = .scaleAspectFill - v.layer.cornerRadius = 24 - v.layer.masksToBounds = true - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(60) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - make.width.height.equalTo(48) - } - return v - }() - - iconAndLabel = { - let v = CLIconLabel() - v.iconImageView.image = MWIconFont.image(fromIcon: .chat, size: CGSize(width: 16, height: 16), color: .text) - v.iconSize = CGSize(width: 16, height: 16) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-24) - make.centerY.equalToSuperview() - } - return v - }() - - rankLabel = { - let v = UILabel() - v.font = .t.tnml - v.textColor = .c.ctpn - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalTo(contentView.snp.leading).offset(30) - make.centerY.equalToSuperview() - } - return v - }() - - contentStackView = { - let v = UIStackView() - v.axis = .vertical - v.alignment = .leading - v.spacing = 4 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(avatarView.snp.trailing).offset(12) - make.trailing.lessThanOrEqualTo(iconAndLabel.snp.leading).offset(-12) - make.centerY.equalToSuperview() - } - return v - }() - - nameLabel = { - let v = CLLabel() - v.font = .t.tts - v.textColor = .c.ctpn - contentStackView.addArrangedSubview(v) - return v - }() - -// descLabel = { -// let v = CLLabel() -// v.font = .t.tls -// v.textColor = .c.ctsn -// contentStackView.addArrangedSubview(v) -// return v -// }() - - descIconAndLabel = { - let v = CLIconLabel() - v.iconImageView.image = MWIconFont.image(fromIcon: .likeFill, size: CGSize(width: 12, height: 12), color: .c.ctsn) - v.contentLabel.font = .t.tls - v.contentLabel.textColor = .c.ctsn - contentStackView.addArrangedSubview(v) - return v - }() - -// #warning("test") -// testData() - } - - func config(_ data: AIDiscoverTop?, type: DiscoverRankType) { - self.data = data - self.type = type - - guard let rank = data else { return } - - // 配置排名 - rankLabel.text = "\(rank.rankNo ?? 0)" - - // 配置头像 - avatarView.loadImage(rank.headImg) - - // 配置名称和描述 - nameLabel.text = rank.nickname ?? "-" - - - //let likesDisplayString = "\(String.displayNumber(NSNumber(value: rank.likedNum ?? 0), scale: 1)) Likes" - //descLabel.text = likesDisplayString - - let likesDisplayString = String.displayNumber(NSNumber(value: rank.likedNum ?? 0), scale: 1) - descIconAndLabel.contentLabel.text = likesDisplayString - - // 配置右侧数值和图标 - var content = "-" - switch type { - case .chat: - content = rank.formateChatDisplay() - case .heartbeat: - content = rank.formateHeartbeatDisplay() - case .gift: - content = rank.formateGiftDisplay() - } - iconAndLabel.contentLabel.text = content - refreshIconLabelIcon() - } - - private func refreshIconLabelIcon(){ - switch type { - case .chat: - iconAndLabel.iconImageView.image = MWIconFont.image(fromIcon: .chat, size: CGSize(width: 16, height: 16), color: .white) - case .heartbeat: - iconAndLabel.iconImageView.image = UIImage(named: "heartbeat") - case .gift: - iconAndLabel.iconImageView.image = UIImage(named: "icon_16_diamond") - } - } - - private func testData() { - iconAndLabel.contentLabel.text = "123" - rankLabel.text = "1" - nameLabel.text = "123" -// descLabel.text = "123" - } -} - -class LeaderboardListTopThreeCell: UITableViewCell { - var topThreeView: DiscoverTopThreeRank! - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - topThreeView = { - let v = DiscoverTopThreeRank() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(12) - make.bottom.equalToSuperview().offset(-12) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - v.setup2nd3rdBottomPdding() - return v - }() - } - - func config(_ data: [AIDiscoverTop]?, type: DiscoverRankType) { - topThreeView.type = type - topThreeView.config(data) - } -} diff --git a/crush/Crush/Src/Modules/Discover/Model/DiscoverHomeRolesViewModel.swift b/crush/Crush/Src/Modules/Discover/Model/DiscoverHomeRolesViewModel.swift deleted file mode 100644 index f9e9a96..0000000 --- a/crush/Crush/Src/Modules/Discover/Model/DiscoverHomeRolesViewModel.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// DiscoverHomeRolesViewModel.swift -// Crush -// -// Created by Leon on 2025/9/13. -// - -/// 每个Character下一个 -class DiscoverHomeRolesViewModel { - /// 固定 - var character: DictNode? - // @Required - var tags: [DictNode]? - - var selectTagCode: String? - - // var filterParams = [String:Any]() - var filterModel : RolesFilterModel? - - func loadRoles(pn: Int, codes: [String], exList: [Int]? = nil, completion: ((_ status: Bool, _ datas: [AIRoleInfo]?) -> Void)?) { - - var request = AIRoleListRequest() - request.pn = pn - if let theCharacterCode = character?.code{ - request.characterCodeList = [theCharacterCode] - } - if codes.count > 0{ - request.tagCodeList = codes - } - request.exList = exList - var params = request.toNonNilDictionary() - - if let filterCondition = filterModel{ - // 筛选条件 - if let sexs = filterCondition.sex, sexs.count > 0, let firstSex = sexs.first{ // 后续可能会改成多选 - params.updateValue(firstSex.rawValue, forKey: "sex") - } - - if let age = filterCondition.age{ - params.updateValue(age.rawValue, forKey: "age") - } - - if let roleCodes = filterCondition.roleCodeList, roleCodes.count > 0{ - params.updateValue(roleCodes, forKey: "roleCodeList") - } - } - - // params = params.merged(with: filterParams) - - DiscoverProvider.request(.homeRolesList(params: params), modelType: [AIRoleInfo].self) { result in - switch result { - case let .success(model): - completion?(true, model) - case .failure: - completion?(false, nil) - } - } - } -} diff --git a/crush/Crush/Src/Modules/Discover/Model/RolesFilterModel.swift b/crush/Crush/Src/Modules/Discover/Model/RolesFilterModel.swift deleted file mode 100644 index 9c4913f..0000000 --- a/crush/Crush/Src/Modules/Discover/Model/RolesFilterModel.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// RolesFilterModel.swift -// Crush -// -// Created by Leon on 2025/9/13. -// - -struct RolesFilterModel:Codable{ - /// 支持多选、单选 - var sex: [Sex]? - var age: AIRoleListRequestAge? - var roleCodeList: [String]? - - // 统计筛选条件数 - func filterCount() -> Int{ - var count = 0 - if let sexCount = sex?.count, sexCount > 0{ - count += sexCount - } - if age != nil{ - count += 1 - } - - count += (roleCodeList ?? []).count - return count - } -} diff --git a/crush/Crush/Src/Modules/Discover/View/DiscoverBanner.swift b/crush/Crush/Src/Modules/Discover/View/DiscoverBanner.swift deleted file mode 100644 index 82bbf12..0000000 --- a/crush/Crush/Src/Modules/Discover/View/DiscoverBanner.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// DiscoverBanner.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -class DiscoverBanner:UIView{ - - var adBlock:UIView! - var imageView: UIImageView! - var titleLabel: UILabel! - var topButton: UIButton! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - adBlock = { - let v = UIView() - v.cornerRadius = 16 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.bottom.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(147.0/345.0) - } - return v - }() - - imageView = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - v.clipsToBounds = true - v.image = UIImage(named: "discover_banner_ad") - adBlock.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .text - v.numberOfLines = 0 - adBlock.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - topButton = { - let v = UIButton() - adBlock.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - //titleLabel.text = "Dialy Free Crush Coin" - } -} diff --git a/crush/Crush/Src/Modules/Discover/View/DiscoverHeadLeaderBoardEntryView.swift b/crush/Crush/Src/Modules/Discover/View/DiscoverHeadLeaderBoardEntryView.swift deleted file mode 100644 index 6c99531..0000000 --- a/crush/Crush/Src/Modules/Discover/View/DiscoverHeadLeaderBoardEntryView.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// DiscoverHeadLeaderBoardEntryView.swift -// Crush -// -// Created by Leon on 2025/9/9. -// -import JXSegmentedView -import UIKit -class DiscoverHeadLeaderBoardEntryView: UIView { - var topBgView: UIImageView! - var entryButton: EPIconTertiaryButton! - var titleLabel: CLLabel! - - let segmentedViewHeight = 32 - lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - private let dataSource = JXSegmentedTagStyleDataSource() - - var topThreeView: DiscoverTopThreeRank! - - var types = [DiscoverRankType.chat, DiscoverRankType.heartbeat, DiscoverRankType.gift] - - var data: DiscoverHomeInfo? - var selectIndex : Int = 0 - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.cbdi - cornerRadius = 16 - - topBgView = { - let v = UIImageView() - v.image = UIImage(named: "discover_home_bg_rank") - addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(200.0/393.0) - } - return v - }() - - entryButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .arrowRight) - v.addTarget(self, action: #selector(tapEntryButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 32, height: 32)) - make.top.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel = { - let v = CLLabel() - v.font = .t.ttm - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.centerY.equalTo(entryButton) - } - return v - }() - - do { - // dataSource.titles = ["Chat", "Heartbeat", "Gift"] - segmentedView.contentEdgeInsetLeft = 16 - segmentedView.contentEdgeInsetRight = 16 - addSubview(segmentedView) - segmentedView.snp.makeConstraints { make in - make.height.equalTo(32) - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(entryButton.snp.bottom).offset(16) - } - segmentedView.dataSource = dataSource - segmentedView.delegate = self - } - - topThreeView = { - let v = DiscoverTopThreeRank() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(segmentedView.snp.bottom).offset(16) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel.text = "Leaderboards" - } - - private func setupData() { - var titles = [String]() - for per in types { - titles.append(per.localizedText) - } - dataSource.titles = titles -// dataSource.titles = ["Chat", "Heartbeat", "Gift"] - segmentedView.reloadData() - } - - private func setupEvent() { - } - - // MARK: - Public - - func config(_ data: DiscoverHomeInfo?){ - self.data = data - - refreshRankData() - } - - // MARK: - Helper - - func refreshRankData(){ - guard let info = data else{ - return - } - - topThreeView.type = types[segmentedView.selectedIndex] - - if segmentedView.selectedIndex == 0{ - topThreeView.config(info.aiChatRankTop3List) - }else if segmentedView.selectedIndex == 1{ - topThreeView.config(info.aiHeartbeatRankTop3List) - }else if segmentedView.selectedIndex == 2{ - topThreeView.config(info.aiGiftRankTop3List) - } - } - - // MARK: - Action - - @objc private func tapEntryButton(){ - AppRouter.goLeaderboardPager() - } -} - -extension DiscoverHeadLeaderBoardEntryView: JXSegmentedViewDelegate{ - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int){ - if selectIndex != index{ - selectIndex = index - refreshRankData() - } - } -} diff --git a/crush/Crush/Src/Modules/Discover/View/DiscoverHeadPinHeader.swift b/crush/Crush/Src/Modules/Discover/View/DiscoverHeadPinHeader.swift deleted file mode 100644 index 7e8bcb6..0000000 --- a/crush/Crush/Src/Modules/Discover/View/DiscoverHeadPinHeader.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// DiscoverHeadPinHeader.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -import JXSegmentedView -class DiscoverHeadPinHeader: UIView { - let segmentedViewHeight = 48 - - var titleLabel: CLLabel! - var filterButton: EPIconGhostButton! - lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - // lazy var subSegementedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(56))) - - var dataSource = JXSegmentedTitleDataSource() - // var subDataSouce = JXSegmentedTagStyleDataSource() - - var filterConditionCount : Int = 0 - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { -// backgroundColor = .random - - titleLabel = { - let v = CLLabel() - v.font = .t.ttm - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalToSuperview().offset(12) - } - return v - }() - - filterButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .filterFill) - v.setBackgroundImage(nil, for: .highlighted) - addSubview(v) - v.snp.makeConstraints { make in - //make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-24) - make.centerY.equalTo(titleLabel) - } - return v - }() - - do { - addSubview(segmentedView) - segmentedView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(48) - make.height.equalTo(segmentedViewHeight) - } - } - -// do { -// addSubview(subSegementedView) -// subSegementedView.snp.makeConstraints { make in -// make.leading.trailing.equalToSuperview() -// make.height.equalTo(56) -// make.top.equalTo(segmentedView.snp.bottom).offset(0) -// } -// } - - titleLabel.text = "Classification" - } - - private func setupData() { - dataSource.clNormalStyle() - segmentedView.dataSource = dataSource - segmentedView.clNormalStyle() - -// subSegementedView.contentEdgeInsetLeft = 24 -// subSegementedView.contentEdgeInsetRight = 24 -// subSegementedView.dataSource = subDataSouce - - filterButton.titleLabel?.font = .t.tll - filterButton.setTitleColor(.text, for: .normal) - filterButton.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - filterButton.layer.masksToBounds = false - } - - private func setupEvent() { - - } - - // MARK: Helper - - // MARK: Public - func setupFilterCount(_ count: Int){ - filterConditionCount = count - if count > 0{ - filterButton.optionalIconColor = .text - filterButton.setTitle(" (\(count))", for: .normal) - }else{ - filterButton.optionalIconColor = .text - filterButton.setTitle("", for: .normal) - } - filterButton.layoutIfNeeded() - } -} diff --git a/crush/Crush/Src/Modules/Discover/View/DiscoverHeadView.swift b/crush/Crush/Src/Modules/Discover/View/DiscoverHeadView.swift deleted file mode 100644 index 661d117..0000000 --- a/crush/Crush/Src/Modules/Discover/View/DiscoverHeadView.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// DiscoverHeadView.swift -// Crush -// -// Created by Leon on 2025/9/8. -// - -import JXSegmentedView -import UIKit - -class DiscoverHeadView: UIView { - var banner: DiscoverBanner! - var leaderboardEntryView: DiscoverHeadLeaderBoardEntryView! - var heightChangeBlock: ((CGFloat) -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - banner = { - let v = DiscoverBanner() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.statusBarHeight + 24) - } - return v - }() - - leaderboardEntryView = { - let v = DiscoverHeadLeaderBoardEntryView() - addSubview(v) - v.snp.makeConstraints { make in - //make.top.equalToSuperview().offset(UIWindow.statusBarHeight + 24) - make.top.equalTo(banner.snp.bottom).offset(24) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - } - - private func setupData() { -// #warning("test") -// leaderboardEntryView.topThreeView.showEmpty(true) - } - - private func setupEvent() { - } - - override func layoutSubviews() { - super.layoutSubviews() - let maxY = leaderboardEntryView.frame.maxY + 24 - heightChangeBlock?(maxY) - } -} diff --git a/crush/Crush/Src/Modules/Discover/View/DiscoverRankViews.swift b/crush/Crush/Src/Modules/Discover/View/DiscoverRankViews.swift deleted file mode 100644 index d8892d7..0000000 --- a/crush/Crush/Src/Modules/Discover/View/DiscoverRankViews.swift +++ /dev/null @@ -1,363 +0,0 @@ -// -// DiscoverRankViews.swift -// Crush -// -// Created by Leon on 2025/9/8. -// - -import SnapKit -import UIKit - -enum DiscoverRankType { - case chat - case heartbeat - case gift - - var localizedText: String { - switch self { - case .chat: - return "Chat" - case .heartbeat: - return "Heartbeat" - case .gift: - return "Gift" - } - } -} - -class DiscoverTopThreeRank: UIView { - var stackH: UIStackView! - - var secondFlag = DiscoverTopThreeRankFlagView() - var firstFlag = DiscoverTopThreeRankFlagView() - var thirdFlag = DiscoverTopThreeRankFlagView() - - var consTopToSuperForSecondFlag: Constraint? - var consBottomToSuperForSecondFlag: Constraint? - - var consTopToSuperForThirdFlag: Constraint? - var consBottomToSuperForThirdFlag: Constraint? - - var type: DiscoverRankType = .chat{ - didSet{ - secondFlag.type = type - firstFlag.type = type - thirdFlag.type = type - } - } - var datas: [AIDiscoverTop]? - - override init(frame: CGRect) { - super.init(frame: frame) - - stackH = { - let v = UIStackView() - v.spacing = 4 - v.distribution = .fillEqually - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.top.bottom.equalToSuperview() - make.height.equalTo(211) - } - return v - }() - - firstFlag.setupRankPlace(1) - secondFlag.setupRankPlace(2) - thirdFlag.setupRankPlace(3) - - - stackH.addArrangedSubview(secondFlag) - secondFlag.snp.makeConstraints { make in - self.consBottomToSuperForSecondFlag = make.bottom.equalToSuperview().constraint - self.consTopToSuperForSecondFlag = make.top.equalToSuperview().offset(45).constraint - } - - stackH.addArrangedSubview(firstFlag) - firstFlag.snp.makeConstraints { make in - make.bottom.equalToSuperview() - make.top.equalToSuperview() - } - - stackH.addArrangedSubview(thirdFlag) - thirdFlag.snp.makeConstraints { make in - self.consBottomToSuperForSecondFlag = make.bottom.equalToSuperview().constraint - self.consTopToSuperForThirdFlag = make.top.equalToSuperview().offset(45).constraint - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Public - - func showEmpty(_ empty: Bool) { - if empty { - stackH.isHidden = true - showEmpty(text: "No character yet") - } else { - stackH.isHidden = false - hideEmpty() - } - } - - func setup2nd3rdBottomPdding() { - secondFlag.snp.updateConstraints { make in - make.top.equalToSuperview().offset(14) - make.bottom.equalToSuperview().offset(-24) - } - thirdFlag.snp.updateConstraints { make in - make.top.equalToSuperview().offset(14) - make.bottom.equalToSuperview().offset(-24) - } - - setNeedsLayout() - layoutIfNeeded() - } - - // MARK: - Public - - func config(_ data: [AIDiscoverTop]?) { - datas = data - - firstFlag.config(getRankData(no: 1)) - secondFlag.config(getRankData(no: 2)) - thirdFlag.config(getRankData(no: 3)) - } - - // MARK: Helper - - private func getRankData(no: Int) -> AIDiscoverTop? { - guard let validDatas = datas else { return nil } - for per in validDatas { - if per.rankNo ?? 0 == no { - return per - } - } - return validDatas.first - } -} - -/// Top three flag -class DiscoverTopThreeRankFlagView: UIView { - - var cornerBlock: UIView! - var imageView: UIImageView! -// var maskGradient: GradientView! - var topShadowIv : UIImageView! - var overlay: UIImageView! - var iconAndLabel: CLIconLabel! - var rankLabel: UILabel! - var topButton: UIButton! - - var top1: Bool = false { - didSet { - imageView.snp.makeConstraints { make in - make.top.equalToSuperview() - } - } - } - - var type: DiscoverRankType = .chat { - didSet{ - refreshIconLabelIcon() - } - } - var data: AIDiscoverTop? - - override init(frame: CGRect) { - super.init(frame: frame) - - //layer.masksToBounds = false - cornerRadius = 8 - - cornerBlock = { - let v = UIView() - //v.cornerRadius = 8 - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - imageView = { - let v = UIImageView() - v.cornerRadius = 8 - v.contentMode = .scaleAspectFill - cornerBlock.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - - topShadowIv = { - let v = UIImageView() - v.image = UIImage(named: "discover_rank_flag_overlay") - v.contentMode = .scaleToFill - addSubview(v) - v.snp.makeConstraints { make in - make.leading.top.trailing.equalToSuperview() - make.height.equalTo(40) - } - return v - }() - -// maskGradient = { -// let v = GradientView(colors: [UIColor.c.cbdi.withAlphaComponent(1), UIColor.c.cbdi.withAlphaComponent(0)], gradientType: .topToBottom) -// addSubview(v) -// v.snp.makeConstraints { make in -// make.leading.top.trailing.equalToSuperview() -// make.height.equalTo(40) -// } -// return v -// }() - - overlay = { - let v = UIImageView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - rankLabel = { - let v = UILabel() - v.textColor = .white - v.font = .t.tnds - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-8) - } - return v - }() - - iconAndLabel = { - let v = CLIconLabel() - - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalTo(rankLabel.snp.top).offset(-2) - } - return v - }() - - topButton = { - let v = UIButton() - v.addTarget(self, action: #selector(tapTopButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - //testData() - } - - private func testData() { - rankLabel.text = "1st" - imageView.image = UIImage(named: "egpic") - iconAndLabel.iconImageView.image = UIImage(named: "heartbeat") - iconAndLabel.contentLabel.text = "37.8℃" - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func refreshIconLabelIcon(){ - switch type { - case .chat: - iconAndLabel.iconImageView.image = MWIconFont.image(fromIcon: .chat, size: CGSize(width: 16, height: 16), color: .white) - case .heartbeat: - iconAndLabel.iconImageView.image = UIImage(named: "heartbeat") - case .gift: - iconAndLabel.iconImageView.image = UIImage(named: "icon_16_diamond") - } - } - - // MARK: Public - - func setupRankPlace(_ rank: Int){ - if rank == 1{ - setupOverlayImage(UIImage(named: "leaderboard_flag_top1")!) - }else if rank == 2{ - setupOverlayImage(UIImage(named: "leaderboard_flag_top2")!) -// topShadowIv.snp.updateConstraints { make in -// make.top.equalToSuperview().offset(-20) -// } - }else if rank == 3{ - setupOverlayImage(UIImage(named: "leaderboard_flag_top3")!) -// topShadowIv.snp.updateConstraints { make in -// make.top.equalToSuperview().offset(-20) -// } - } - } - - func config(_ data: AIDiscoverTop?) { - self.data = data - guard let rank = data else { return } - - rankLabel.text = rank.formateRankDisplayString() - imageView.loadImage(rank.homeImageUrl) - var content = "-" - switch type { - case .chat: - content = rank.formateChatDisplay() - case .heartbeat: - content = rank.formateHeartbeatDisplay() - case .gift: - content = rank.formateGiftDisplay() - } - iconAndLabel.contentLabel.text = content - } - - // MARK: - Helper - - private func setupOverlayImage(_ image: UIImage) { - overlay.image = image - let aspectRatio = image.size.height / image.size.width - overlay.snp.makeConstraints { make in - make.height.equalTo(overlay.snp.width).multipliedBy(aspectRatio) - } - } - - // MARK: - Action - - @objc private func tapTopButton(){ - guard let rank = data, let aiId = rank.aiId else { return } - //AppRouter.goAIRoleHome(aiId: aiId) - AppRouter.goChatVC(aiId: aiId) - } -} - -class DiscoverHomeRankEntry: UIView { - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - } - - private func setupData() { - } - - private func setupEvent() { - } -} diff --git a/crush/Crush/Src/Modules/Discover/View/DiscoverRootPageView.swift b/crush/Crush/Src/Modules/Discover/View/DiscoverRootPageView.swift deleted file mode 100644 index cf193ce..0000000 --- a/crush/Crush/Src/Modules/Discover/View/DiscoverRootPageView.swift +++ /dev/null @@ -1,231 +0,0 @@ -// -// DiscoverRootPageView.swift -// -// -// Created by Leon on 2025/7/22. -// - -import JXPagingView -import JXSegmentedView -import UIKit - -class DiscoverRootPageView: CLContainer { - private var bgBdView : UIView! - private lazy var pagingView = JXPagingListRefreshView(delegate: self) - - lazy var headerView: DiscoverHeadView = DiscoverHeadView() - lazy var pinHeaderView: DiscoverHeadPinHeader = DiscoverHeadPinHeader() - - private var headerViewHeight: CGFloat = 288 - private var headerPinHeadHeight: Int = 96 - - private var controllers = [JXPagingViewListViewDelegate]() - - // Data - var characterNodes: [DictNode] = [DictNode]() - var tagsNodes: [DictNode] = [DictNode]() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bgBdView = { - let v = UIView() - v.backgroundColor = .c.cbd - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - v.alpha = 0 - return v - }() - - pagingView.mainTableView.backgroundColor = UIColor.clear - pagingView.mainTableView.gestureDelegate = self - pagingView.pinSectionHeaderVerticalOffset = Int(UIWindow.statusBarHeight) - pagingView.listContainerView.listCellBackgroundColor = .clear - addSubview(pagingView) - pagingView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - make.top.equalToSuperview() - } - - pinHeaderView.segmentedView.listContainer = pagingView.listContainerView - pinHeaderView.segmentedView.delegate = self - //pinHeaderView.subSegementedView.delegate = self - } - - // 初始化 - private func setupData() { - guard let characterNodes = AppDictManager.shared.aiDict?.characterDictList else { - return - } - - self.characterNodes = characterNodes - - controllers.removeAll() - var titles = [String]() - - // First Tab: All - do{ - titles.append("All") - let vc = DiscoverRolesGridController() - vc.allCatogryTab = true - // 一些默认情况 - let viewModel = DiscoverHomeRolesViewModel() - viewModel.character = nil - vc.viewModel = viewModel - controllers.append(vc) - } - - for per in characterNodes { - titles.append(per.name) - let vc = DiscoverRolesGridController() - // 一些默认情况 - let viewModel = DiscoverHomeRolesViewModel() - viewModel.character = per - viewModel.tags = per.childDictList - viewModel.selectTagCode = per.childDictList?.first?.code - vc.viewModel = viewModel - controllers.append(vc) - } - pinHeaderView.dataSource.titles = titles - - refreshSubTagsSegmentView() - } - - /// 仅刷新父标题文案 - private func refreshSubTagsSegmentView() { - let titleIndex = pinHeaderView.segmentedView.selectedIndex - - //⚠️ First tab is All - if titleIndex > 0{ - let selectCharacterNode = characterNodes[titleIndex - 1] - tagsNodes = selectCharacterNode.childDictList ?? [] - }else{ - tagsNodes = [] - } - // let selectTagNode = tagsNodes[pinHeaderView.subSegementedView.selectedIndex] - -// var titles = [String]() -// for per in tagsNodes { -// titles.append("#\(per.name)") -// } -// pinHeaderView.subDataSouce.titles = titles -// pinHeaderView.subSegementedView.reloadData() - } - - private func setupEvent() { - headerView.heightChangeBlock = { [weak self] height in - self?.headerViewHeight = height - self?.pagingView.resizeTableHeaderViewHeight() - } - - // pinHeaderView.filterButton.addTarget(self, action: #selector(tapFilterButton(button:)), for: .touchUpInside) - } - - // MARK: - Public - - func setupFilter(filter: RolesFilterModel){ - dlog("当前: params:\(filter.toNonNilDictionary())") - for (index, vc) in controllers.enumerated() { - if let gridVc = vc as? DiscoverRolesGridController{ -// #warning("test") - gridVc.viewModel.filterModel = filter - if index == pinHeaderView.segmentedView.selectedIndex{ - gridVc.loadNewData() - }else{ - gridVc.refreshDatasWhenAppear = true - } - } - } - } - - // MARK: Action - -// @objc private func tapFilterButton(button: UIButton){ -// if pinHeaderView.filterConditionCount >= 2{ -// pinHeaderView.setupFilterCount(0) -// }else{ -// pinHeaderView.setupFilterCount(2) -// } -// } -} - -extension DiscoverRootPageView { -} - -extension DiscoverRootPageView: JXPagingViewDelegate, JXSegmentedViewDelegate { - func tableHeaderViewHeight(in _: JXPagingView) -> Int { - return Int(headerViewHeight) - } - - func tableHeaderView(in _: JXPagingView) -> UIView { - return headerView - } - - func heightForPinSectionHeader(in _: JXPagingView) -> Int { - return headerPinHeadHeight - } - - func viewForPinSectionHeader(in _: JXPagingView) -> UIView { - return pinHeaderView - } - - func numberOfLists(in _: JXPagingView) -> Int { - return controllers.count - } - - func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { - let vc = controllers[index] - return vc - } - - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { - if segmentedView == pinHeaderView.segmentedView { - refreshSubTagsSegmentView() - } -// else if segmentedView == pinHeaderView.subSegementedView { -// let selectTag = tagsNodes[index] -// if let currentVc = controllers[pinHeaderView.segmentedView.selectedIndex] as? DiscoverRolesGridController { -// currentVc.tryLoad(code: selectTag.code!) -// } -// } - } - - func mainTableViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviViewsAlpha(scrollView: scrollView, alphaViews: [bgBdView, navigationView?.bgView], oppositeViews: []) - } -} - -extension DiscoverRootPageView: JXPagingMainTableViewGestureDelegate { - func mainTableViewGestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - // 如果是 UICollectionView 的手势,先判断滚动方向 - if let panGesture = otherGestureRecognizer as? UIPanGestureRecognizer, - let otherView = otherGestureRecognizer.view, - otherView is UICollectionView { - let velocity = panGesture.velocity(in: otherView) - // 横向滚动时禁止 - if abs(velocity.x) > abs(velocity.y) { - return false - } - } - // 禁止segmentedView左右滑动的时候,上下和左右都可以滚动 -// if otherGestureRecognizer == segmentedView.collectionView.panGestureRecognizer { -// return false -// } - return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - } -} diff --git a/crush/Crush/Src/Modules/Friend/FriendsRootHomeController.swift b/crush/Crush/Src/Modules/Friend/FriendsRootHomeController.swift deleted file mode 100644 index 2a5c8ff..0000000 --- a/crush/Crush/Src/Modules/Friend/FriendsRootHomeController.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// FriendsRootHomeController.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import JXPagingView -import JXSegmentedView -import UIKit - -class FriendsRootHomeController: CLTabRootController { - private var naviMoreButton: EPIconGhostButton! - - private let segmentedViewHeight = 44 - - private lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - private lazy var listContainerView = JXSegmentedListContainerView(dataSource: self) - - private let dataSource = JXSegmentedTitleDataSource() - private let titles = ["Message", "Friends"] - - lazy var messageListVc = SessionListController() - lazy var friendsListVc = FrinendsListController() - - override func viewDidLoad() { - super.viewDidLoad() - - // view.showEmpty(text: "Friends Coming soon") - - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - navigationView.bgView.alpha = 0 - - naviMoreButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .chatroomMore) - v.addTarget(self, action: #selector(tapMore(sender:)), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 52, height: 44)) - } - return v - }() - - setupJXViews() - } - - private func setupJXViews(){ - dataSource.naviTitleStyle() - dataSource.titles = titles - - segmentedView.naviTitleStyle() - segmentedView.dataSource = dataSource - segmentedView.delegate = self - - navigationView.addSubview(segmentedView) - segmentedView.snp.makeConstraints { make in - make.height.equalTo(44) - make.leading.equalToSuperview() - make.bottom.equalToSuperview() - make.trailing.equalTo(naviMoreButton.snp.leading).offset(-8) - } - - listContainerView.contentScrollView().isScrollEnabled = false - view.addSubview(listContainerView) - listContainerView.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - - segmentedView.listContainer = listContainerView - } - - private func setupDats() { - } - - private func setupEvents() { - } - - // MARK: - Action - - @objc private func tapMore(sender: UIButton) { - let pop = CLPopoverListView() - let rect = view.convert(sender.frame, from: sender.superview) - var items = [CLPopoverListTextItem]() -// do { -// let listItem = CLPopoverListTextItem() -// listItem.title = "Notice" -// listItem.image = MWIconFont.image(fromIcon: .messages, size: CGSize(width: 20, height: 20), color: .text) -// listItem.updateLayout() -// listItem.selectedHandler = { _ in -// AppRouter.goNoticeCenter() -// } -// items.append(listItem) -// } - do { - let listItem = CLPopoverListTextItem() - listItem.title = "Search" - listItem.image = MWIconFont.image(fromIcon: .search, size: CGSize(width: 20, height: 20), color: .text) - listItem.updateLayout() - listItem.selectedHandler = {[weak self] _ in - let search = FriendMainSearchController() - self?.presentNaviRootVc(vc: search) - } - items.append(listItem) - } - - do { - let listItem = CLPopoverListTextItem() - listItem.title = "Read All" - listItem.image = MWIconFont.image(fromIcon: .clear, size: CGSize(width: 20, height: 20), color: .text) - listItem.updateLayout() - listItem.selectedHandler = { _ in - IMManager.shared.clearAllUnread() - } - items.append(listItem) - } - - do { - let listItem = CLPopoverListTextItem() - listItem.title = "Delete All" - listItem.image = MWIconFont.image(fromIcon: .iconDelete, size: CGSize(width: 20, height: 20), color: .text) - listItem.updateLayout() - listItem.selectedHandler = {[weak self] _ in - let alert = Alert(title: "删除内容", text: "删除全部消息后,将清空所有的消息记录") - let action1 = AlertAction(title: "删除", actionStyle: .confirm) {[weak self] in - self?.messageListVc.deleteAllConversations() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - - } - items.append(listItem) - } - - pop.setupCommonPopover(rect, inView: view, items: items, block: nil) - } -} - -extension FriendsRootHomeController: JXSegmentedViewDelegate { - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { - } -} - -extension FriendsRootHomeController: JXSegmentedListContainerViewDataSource { - func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int { - return titles.count - } - - func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate { - if index == 0 { - return messageListVc - } else { - return friendsListVc - } - } -} diff --git a/crush/Crush/Src/Modules/Friend/FrinendsListController.swift b/crush/Crush/Src/Modules/Friend/FrinendsListController.swift deleted file mode 100644 index d2a55c5..0000000 --- a/crush/Crush/Src/Modules/Friend/FrinendsListController.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// FrinendsListController.swift -// Crush -// -// Created by Leon on 2025/8/13. -// - -import JXSegmentedView -import UIKit -class FrinendsListController: CLBaseTableController { - lazy var headView: FriendsHeartBeatHead = { - let headView = FriendsHeartBeatHead() - headView.frame = CGRectMake(0, 0, UIScreen.width, 0) // 60 - - headView.heightChangeBlock = { [weak self] height in - self?.headView.height = height - self?.tableView.tableHeaderView = self?.headView - } - headView.isHidden = true - return headView - }() - - override func viewDidLoad() { - super.viewDidLoad() - navigationView.isHidden = true - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - if headView.noticeOnceView.isHidden == false { - headView.noticeOnceView.tapClose() - } - } - - private func setupViews() { - view.backgroundColor = .clear - tableView.register(FriendsListCell.self, forCellReuseIdentifier: "FriendsListCell") - tableView.tableHeaderView = headView - - addRefreshHeaderFooter() - - loadData() - - if let friendsHeartBeatNoticeConfig = AppCache.fetchCache(key: CacheKey.friendsHeartBeatShowedOnce.rawValue, type: Bool.self), friendsHeartBeatNoticeConfig == true{ - headView.showNoticeOnce(false) - }else{ - headView.showNoticeOnce(true) - } - } - - private func setupDatas() { - - } - - override func loadData() { - let pageData = RequestPageData() - pageData.pn = page - var request = FriendsListRequest() - request.page = pageData - let params = request.toNonNilDictionary() - - FriendsProvider.request(.heartbeatRelationList(params: params), modelType: ResponseContentPageData.self) { [weak self] result in - self?.tableView.mj_header?.endRefreshing() - self?.tableView.mj_footer?.endRefreshing() - switch result { - case let .success(model): - if let validDatas = model?.datas { - if pageData.pn == 1 { - self?.datas = validDatas - self?.tableView.mj_footer?.resetNoMoreData() - self?.view.setupEmpty(empty: validDatas.count <= 0, msg: "暂无关系") - } else { - self?.datas.append(contentsOf: validDatas) - if validDatas.count <= 0 { - self?.tableView.mj_footer?.endRefreshingWithNoMoreData() - } - } - self?.tableView.reloadData() - } - case .failure: - break - } - } - - if page == 1{ - loadHeartBeatRank() - } - } - - func loadHeartBeatRank(){ - AIRoleProvider.request(.heartBeatRankGet, modelType: CGFloat.self) {[weak self] result in - switch result { - case .success(let model): - self?.headView.isHidden = false - self?.headView.config(percent: model) - case .failure: - break - } - } - } - - private func setupEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(notifiyRelationHiddenUpdate), name: AppNotificationName.heartbeatRelationHiddenUpdate.notificationName, object: nil) - } - - // MARK: - Override - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return datas.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "FriendsListCell", for: indexPath) as! FriendsListCell - cell.delegate = self - if let data = datas[indexPath.row] as? RelationFriend { - cell.config(data) - } - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let data = datas[indexPath.row] as? RelationFriend, let aiId = data.aiId else{ - return - } - AppRouter.goAIRoleHome(aiId: Int(aiId)) - } -} - -extension FrinendsListController { - @objc func notifiyRelationHiddenUpdate(){ - loadNewData() - } -} - -extension FrinendsListController: FriendsListCellDelegate{ - func friendsCellTapChat(aiId: Int) { - AppRouter.goChatVC(aiId: aiId) - } -} - -extension FrinendsListController: JXSegmentedListContainerViewListDelegate { - func listView() -> UIView { - return view - } -} diff --git a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/HeartBeatLevelGridController.swift b/crush/Crush/Src/Modules/Friend/HeartBeatLevel/HeartBeatLevelGridController.swift deleted file mode 100644 index 2b4436a..0000000 --- a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/HeartBeatLevelGridController.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// HeartBeatLevelGridController.swift -// Crush -// -// Created by Leon on 2025/8/16. -// - -import UIKit - -class HeartBeatLevelGridController: CLViewController { - var aiId: Int = 0 - - var relation: AIUserHeartBeatRelation? - var heartBeatLevelAccessDicts: [HeartbeatLeveLDict] = [HeartbeatLeveLDict]() - - - lazy var popover = PopoverHideRalationView() - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - //navigationView.alpha0Title = "Crush Level" - navigationView.title = "Crush Level" - navigationView.bgView.alpha = 0 - let queryButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .faq) - v.addTarget(self, action: #selector(queryAction), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 44, height: 44)) - } - return v - }() - - let moreButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .more) - v.addTarget(self, action: #selector(moreAction(sender:)), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 44, height: 44)) - } - return v - }() - queryButton.isHidden = false - moreButton.isHidden = false - } - - private func setupDatas() { - loadHeartBeatLevel() - } - - private func loadHeartBeatLevel(){ - Hud.showIndicator() - AIRoleProvider.request(.heartBeatLevelGet(aiId: aiId), modelType: HeartBeatLevelRelation.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - self?.heartBeatLevelAccessDicts = model?.heartbeatLeveLDictList ?? [] - self?.relation = model?.aiUserHeartbeatRelation - self?.reloadViewDatas() - case .failure: - break - } - } - } - - private func setupEvents() { - container.headView.retrieveButton.addTarget(self, action: #selector(tapRetrieveButton), for: .touchUpInside) - popover.controlSwitch.addTarget(self, action: #selector(tapPopoverControlSwitch), for: .valueChanged) - NotificationCenter.default.addObserver(self, selector: #selector(notifiyRelationInfoUpdate), name: AppNotificationName.aiRoleRelationInfoUpdated.notificationName, object: nil) - } - - @objc func notifiyRelationInfoUpdate(){ - loadHeartBeatLevel() - } - - // MARK: Functions - private func reloadViewDatas(){ - container.headView.config(relation) - container.config(heartBeatLevelAccessDicts) - - popover.controlSwitch.isOn = !(relation?.isShow ?? false) - //.. - } - - // MARK: Action - - @objc private func queryAction() { - let content = "*通过聊天或送礼增加心动值,24小时不联系心动值会自动扣减\n\n*心动值会提升心动等级,通过升级解锁称号,功能,以及不同的角色对话阶段\n\n *虚拟角色会根据对话的情绪感受,酌情判断增加或者减少心动值" - let alert = Alert(title: "Tips", text: content) - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) - alert.addAction(action1) - alert.show() - } - - @objc private func moreAction(sender: UIButton) { - let popover = p_createPopoverView() - popover.present(fromView: sender) - } - - @objc private func tapRetrieveButton(){ - let sheet = RetrieveHeartbeatSheet() - sheet.aiId = aiId - sheet.config(self.relation) - sheet.show() - } - - @objc private func tapPopoverControlSwitch(sender: UISwitch) { - let onOfSwitch = !sender.isOn - //dlog("on\(onOfSwitch)") - Hud.showIndicator() - AIRoleProvider.request(.heartBeatRelationSwitch(aiId: aiId, isShow: onOfSwitch), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.loadHeartBeatLevel() // Refresh - NotificationCenter.post(name: .heartbeatRelationHiddenUpdate) - case .failure: - sender.isOn = !onOfSwitch - break - } - } - } - - private func p_createPopoverView() -> FSPopoverView { - let popoverView = FSPopoverView() - popoverView.dataSource = self - popoverView.showsArrow = false - popoverView.showsDimBackground = false - return popoverView - } -} - -extension HeartBeatLevelGridController: FSPopoverViewDataSource { - func contentView(for popoverView: FSPopoverView) -> UIView? { - return popover - } - - func contentSize(for popoverView: FSPopoverView) -> CGSize { - // return CGSize(width: 100, height: 60) - let calSize = popover.label.sizeThatFits(CGSize(width: CGFLOAT_MAX, height: 20)) - let size = CGSize(width: calSize.width + 16 + 84, height: 60) - dlog("hoee: \(size)") - return size - } - - func containerSafeAreaInsets(for popoverView: FSPopoverView) -> UIEdgeInsets { - var insets = view.safeAreaInsets - insets.left = 10.0 - insets.right = 10.0 - return insets - } -} - -class PopoverHideRalationView: UIView { - var controlSwitch: UISwitch! - var label: UILabel! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - snp.makeConstraints { make in - make.height.equalTo(60) - } - controlSwitch = { - let v = UISwitch() - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - return v - }() - - label = { - let v = UILabel() - v.font = .t.tll - v.textColor = .text - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(16) - make.trailing.equalTo(controlSwitch.snp.leading).offset(-16) - } - return v - }() - - label.text = "Hide Relations" - } -} diff --git a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatAnimationView.swift b/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatAnimationView.swift deleted file mode 100644 index 6d5448d..0000000 --- a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatAnimationView.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// HeartBeatAnimationView.swift -// Crush -// -// Created by Leon on 2025/8/19. -// - -import UIKit -import Lottie - -class HeartBeatAnimationView: UIView{ - var bottomBg : AutoRatioImageView! - var topOverlay: UIImageView! - var bottomOverlay: UIImageView! - - var lottieView: LottieAnimationView! - - var percent: CGFloat = 0{ - didSet{ - dlog("Heart animation percent:\(percent)") - changeWavePositionBy(percent: percent) - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bottomBg = { - let v = AutoRatioImageView() -// v.image = UIImage(named: "heart_wave_bg") - v.setImage( UIImage(named: "heart_wave_bg")) - addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - } - return v - }() - - topOverlay = { - let v = UIImageView() - v.image = UIImage(named: "heart_wave_overlay_top") - addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(234/393.0) - } - return v - }() - - bottomOverlay = { - let v = UIImageView() - v.image = UIImage(named: "heart_wave_overlay_bottom") - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(topOverlay.snp.bottom) - make.leading.bottom.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(166/393.0) - } - return v - }() - - lottieView = { - let animation = LottieAnimation.named("heart_beat_wave_lite") - let animationView = LottieAnimationView(animation: animation) - - - animationView.contentMode = .scaleAspectFit - animationView.loopMode = .loop - animationView.backgroundBehavior = .pauseAndRestore - animationView.backgroundColor = .clear - animationView.size = CGSize(width: 120, height: 120) - //addSubview(animationView) - //insertSubview(animationView, at: 0) - insertSubview(animationView, aboveSubview: bottomBg) - animationView.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 120, height: 120)) - make.centerX.equalToSuperview() - make.bottom.equalTo(topOverlay).offset(60) - } - return animationView - }() - } - - private func setupData(){ - lottieView.play() - - changeWavePositionBy(percent: 0) - } - - private func setupEvent(){ - - } - - private func changeWavePositionBy(percent:CGFloat){ - lottieView.snp.updateConstraints { make in - // ⚠️配置100 - make.bottom.equalTo(topOverlay).offset(100 * (1-percent)) - } - } -} diff --git a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatLevelGridHeadView.swift b/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatLevelGridHeadView.swift deleted file mode 100644 index 7c1df97..0000000 --- a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatLevelGridHeadView.swift +++ /dev/null @@ -1,381 +0,0 @@ -// -// HeartBeatLevelGridHeadView.swift -// Crush -// -// Created by Leon on 2025/8/16. -// - -import UIKit - -class HeartBeatLevelGridHeadView: UIView{ - var bgIv : UIImageView! - var avatarsHeartContainer:UIView! - //var heartView : UIImageView! // 后面换成lottie - var heartWave: HeartBeatAnimationView! - - var leftAvatar: CLImageView! - var rightAvatar: CLImageView! - - var itemStackV: UIStackView! - var statesStackH : UIStackView! // 1. in itemStackV - var levelLabel : UILabel! - var line: CLLine! - var heartBeatLabel : UILabel! - - var progressStateContainer: UIView! // 2. in itemStackV - var progressStateLabel: UILabel! - - var leftStackV : UIStackView! // 3. in itemStackV - var heartBeatRankStateLabel: LineSpaceLabel! - var retrieveContainer: UIView! - var retrieveLabel: UILabel! - var retrieveButton: TextButton! - - var heightChangeBlock: ((CGFloat) -> Void)? - - var data: AIUserHeartBeatRelation? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - clipsToBounds = true -// bgIv = { -// let v = UIImageView() -// v.image = UIImage(named: "heart_beat_level_bg") -// addSubview(v) -// v.snp.makeConstraints { make in -// make.leading.top.trailing.equalToSuperview() -// make.height.equalTo(v.snp.width).multipliedBy(393.0/400.0) -// } -// return v -// }() - - heartWave = { - let v = HeartBeatAnimationView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.top.trailing.equalToSuperview() - } - return v - }() - - avatarsHeartContainer = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(120) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight + 12) - } - return v - }() -// heartView = { -// let v = UIImageView() -// v.image = UIImage(named: "heart_wave") -// avatarsHeartContainer.addSubview(v) -// v.snp.makeConstraints { make in -//// make.centerY.equalToSuperview() -// make.center.equalToSuperview() -// } -// return v -// }() - - leftAvatar = { - let v = CLImageView(frame: .zero) - let wh = 64.0 - v.layer.cornerRadius = wh*0.5 - v.layer.masksToBounds = true - v.backgroundColor = .c.csbn - avatarsHeartContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - //make.top.equalToSuperview().offset(134) - make.size.equalTo(CGSize(width: wh, height: wh)) - make.centerX.equalToSuperview().offset(-108) - } - return v - }() - - rightAvatar = { - let v = CLImageView(frame: .zero) - let wh = 64.0 - v.layer.cornerRadius = wh*0.5 - v.layer.masksToBounds = true - v.backgroundColor = .c.csbn - avatarsHeartContainer.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: wh, height: wh)) - make.centerY.equalTo(leftAvatar) - make.centerX.equalToSuperview().offset(108) - } - return v - }() - - itemStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 16 - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(avatarsHeartContainer.snp.bottom) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - statesStackH = { - let v = UIStackView() - v.spacing = 16 - v.alignment = .center - itemStackV.addArrangedSubview(v) - return v - }() - - levelLabel = { - let v = UILabel() - v.font = .t.tndm - v.textColor = .text - statesStackH.addArrangedSubview(v) - return v - }() - - line = { - let v = CLLine() - v.backgroundColor = .c.con - statesStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.width.equalTo(1) - make.height.equalTo(18) - } - return v - }() - line.isHidden = false - - heartBeatLabel = { - let v = UILabel() - v.font = .t.tndm - v.textColor = .text - statesStackH.addArrangedSubview(v) - return v - }() - - progressStateContainer = { - let v = UIView() - itemStackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(24) - make.leading.trailing.equalToSuperview() - } - return v - }() - - progressStateLabel = { - let v = UILabel() - v.font = .t.tds - v.textColor = .text - progressStateContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - } - - let leftLine = { - let line1 = UIView() - line1.backgroundColor = .c.con - progressStateContainer.addSubview(line1) - line1.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(0) - make.trailing.equalTo(v.snp.leading).offset(-12) - make.centerY.equalTo(v) - make.height.equalTo(1) - } - return line1 - }() - - let rightLine = { - let line2 = UIView() - line2.backgroundColor = .c.con - progressStateContainer.addSubview(line2) - line2.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(0) - make.leading.equalTo(v.snp.trailing).offset(12) - make.centerY.equalTo(v) - make.height.equalTo(1) - } - return line2 - }() - leftLine.isHidden = false - rightLine.isHidden = false - - return v - }() - - leftStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 24 - itemStackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - heartBeatRankStateLabel = { - let v = LineSpaceLabel() - v.textColor = .text - let typo = CLSystemToken.typography(token: .tbs) - v.textAlignment = .center - v.config(typo) - leftStackV.addArrangedSubview(v) - return v - }() - - retrieveContainer = { - let v = UIView() - v.backgroundColor = .c.csbn - v.layer.cornerRadius = 12 - v.layer.masksToBounds = true - leftStackV.addArrangedSubview(v) - v.isHidden = true - return v - }() - - retrieveButton = { - let v = TextButton() - v.titleLabel?.font = .t.tls - retrieveContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - retrieveLabel = { - let v = UILabel() - v.font = .t.tbs - v.textColor = .text - retrieveContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - make.trailing.lessThanOrEqualTo(retrieveButton.snp.leading).offset(-12) - } - return v - }() - - - progressStateLabel.text = "· Meet ·" - retrieveButton.text = "Retrieve" - } - - - - private func setupData(){ - // testData() - } - - private func testData(){ - heartBeatLabel.text = "35.6℃" - levelLabel.text = "Lv.1" - heartBeatRankStateLabel.text = "8 days of acquaintance | Heartbeat score more than 99.99% of the interlocutors" - retrieveLabel.text = "Deducted cardiac value: -35.0℃" - } - - private func setupEvent(){ - - } - - // MARK: Public - func config(_ relation: AIUserHeartBeatRelation?){ - data = relation - guard let info = relation else{ - return - } - - // 波浪 - let heartPercent = info.getHeartbeatWavePercent() - heartWave.percent = heartPercent - - //.. - leftAvatar.loadImage(info.aiHeadImg) - rightAvatar.loadImage(info.userHeadImg) - - - - - if info.heartbeatLevel != nil{ // 有心动等级 - levelLabel.isHidden = false - line.isHidden = false - levelLabel.text = info.heartbeatLevel?.localizedText - - let value = info.formatHeartBeatVal() - let heartBeatValueAStr = value.withAttributes([.font(UIFont.t.tndm)]) - let ranges = value.matchStrRange(AIUserHeartBeatRelation.unitOfHeartBeatVal()) - if let range = ranges.first{ - let att = [NSAttributedString.Key.font: UIFont.t.tnms] - heartBeatValueAStr.addAttributes(att, range: range) - } - heartBeatLabel.attributedText = heartBeatValueAStr - - if info.isShow.boolValue{ - progressStateContainer.isHidden = false - progressStateLabel.text = "· \(info.heartbeatLevel?.relationType.localizedText ?? "") · " - }else{ - progressStateContainer.isHidden = true - } - }else{ - levelLabel.isHidden = true - line.isHidden = true - - let value = "0℃" - let heartBeatValueAStr = value.withAttributes([.font(UIFont.t.tndm)]) - let ranges = value.matchStrRange(AIUserHeartBeatRelation.unitOfHeartBeatVal()) - if let range = ranges.first{ - let att = [NSAttributedString.Key.font: UIFont.t.tnms] - heartBeatValueAStr.addAttributes(att, range: range) - } - heartBeatLabel.attributedText = heartBeatValueAStr - - progressStateContainer.isHidden = false - progressStateLabel.font = .t.tts - progressStateLabel.text = "No intention yet" - } - - // 描述 - let days = info.dayCount ?? 0 - let heartBeatScore = info.heartbeatScore ?? 0.999999 - heartBeatRankStateLabel.text = "\(days) days of acquaintance | Heartbeat score more than \(heartBeatScore.formatted(decimal: 2, asPercent: true)) of the interlocutors" - - // 扣减值、扣减区域 - let subtractHeartbeatVal = info.fixedSubtractHeartbeatVal - if subtractHeartbeatVal > 0{ - retrieveContainer.isHidden = false - let fixDisplay = info.fixedSubtractHeartbeatValDisplay - retrieveLabel.text = "Deducted cardiac value: -\(fixDisplay)" - } - - layoutIfNeeded() - } - - // MARK: Other - - override func layoutSubviews() { - super.layoutSubviews() - let maxY = itemStackV.frame.maxY + 16 - heightChangeBlock?(maxY) - } -} diff --git a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatLevelGridView.swift b/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatLevelGridView.swift deleted file mode 100644 index 172026f..0000000 --- a/crush/Crush/Src/Modules/Friend/HeartBeatLevel/View/HeartBeatLevelGridView.swift +++ /dev/null @@ -1,237 +0,0 @@ -// -// HeartBeatLevelGridView.swift -// Crush -// -// Created by Leon on 2025/8/16. -// - -import UIKit - -class HeartBeatLevelGridView: CLContainer { - var cv: UICollectionView! - var headView: HeartBeatLevelGridHeadView! - var headHeight: CGFloat = 386 - var layout = UICollectionViewFlowLayout() - - var datas: [HeartbeatLeveLDict] = [HeartbeatLeveLDict]() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - let width = floor((UIScreen.width - 24 * 2 - 16) * 0.5) - let height = 148.0 - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 0, left: 24, bottom: UIWindow.safeAreaBottom, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - // cv.showsVerticalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(HeartBeatLevelGridCell.self, forCellWithReuseIdentifier: "HeartBeatLevelGridCell") - cv.register(UICollectionReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: "UICollectionReusableView") - addSubview(cv) - cv.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - headView = HeartBeatLevelGridHeadView() - } - - private func setupData() { - } - - private func setupEvent() { - headView.heightChangeBlock = { [weak self] height in - self?.headHeight = height - self?.layout.headerReferenceSize = .init(width: UIScreen.width, height: height) - self?.layout.invalidateLayout() - } - } - - func config(_ levels: [HeartbeatLeveLDict]?){ - datas = levels ?? [] - cv.reloadData() - } -} - -extension HeartBeatLevelGridView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HeartBeatLevelGridCell", for: indexPath) as! HeartBeatLevelGridCell - let data = datas[indexPath.item] - cell.config(data) - return cell - } - - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { - let header = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: "UICollectionReusableView", - for: indexPath - ) - if headView.superview == nil || headView.superview != header { - headView.removeFromSuperview() - } - - header.addSubview(headView) - headView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - - return header - } - return UICollectionReusableView() - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - return CGSize(width: UIScreen.width, height: headHeight) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, alphaViews: [navigationView?.bgView] ) // navigationView?.titleLabel, - } -} - -class HeartBeatLevelGridCell: UICollectionViewCell { - var block: UIView! - var medalIv: UIImageView! - var intimacyLabel: UILabel! - var lockIcon: EPIconTertiaryDarkButton! - var nameLabel: UILabel! - - var overlay: GradientView! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.backgroundColor = .c.csbn - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.top.equalToSuperview() - make.height.equalTo(120) - } - return v - }() - - medalIv = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - block.addSubview(v) - v.snp.makeConstraints { make in - //make.edges.equalToSuperview() - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(-6) - make.bottom.equalToSuperview().offset(6) - } - return v - }() - - overlay = { - let gradient = CLSystemToken.gradient(token: .cob) - let v = GradientView(colors: gradient.colors(), gradientType: .topToBottom) - block.addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(36) - make.leading.trailing.bottom.equalToSuperview() - } - return v - }() - - intimacyLabel = { - let v = UILabel() - v.font = .t.tls - v.textColor = .c.ctsn - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(12) - make.bottom.equalToSuperview().offset(-8) - } - return v - }() - - lockIcon = { - let v = EPIconTertiaryDarkButton(radius: .rectangle, iconSize: .small, iconCode: .iconPrivateBorder) - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - - nameLabel = { - let v = UILabel() - v.font = .t.tlm - v.textColor = .text - v.textAlignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(block.snp.bottom).offset(8) - } - return v - }() - } - - private func setupData() { - intimacyLabel.text = "0.5℃" - nameLabel.text = "Meet" - } - - private func setupEvent() { - } - - // MARK: Public - - func config(_ data: HeartbeatLeveLDict){ - medalIv.loadImage(data.imgUrl) - - nameLabel.text = data.name - lockIcon.isHidden = data.isUnlock.boolValue - intimacyLabel.text = "\((data.startVal ?? 0.0).formatted(decimal: 1))℃" - - if data.isUnlock.boolValue{ - nameLabel.textColor = .text - intimacyLabel.textColor = .text - }else{ - nameLabel.textColor = .c.ctpd - intimacyLabel.textColor = .c.ctsn - } - } -} diff --git a/crush/Crush/Src/Modules/Friend/Model/FriendsModel.swift b/crush/Crush/Src/Modules/Friend/Model/FriendsModel.swift deleted file mode 100644 index 2072dbb..0000000 --- a/crush/Crush/Src/Modules/Friend/Model/FriendsModel.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// FriendsModel.swift -// Crush -// -// Created by Leon on 2025/8/28. -// - -struct FriendsListRequest: Codable{ - var nickname: String? - var page: RequestPageData? -} - - -struct RelationFriend: Codable{ - var heartbeatLevel: HeartbeatLevel? // "LEVEL_3" - var sex: Int? // 1 - var characterName: String? // "撩人" - var heartbeatLevelNum: Int? // 3 - var aiId: Int64? // 439257063882753 - var userId: Int64? // 439213911113729 - var birthday: Int64? // 963072000000 (毫秒级时间戳) - var roleName: String? // "原创" - var tagName: String? // "温柔" - var heartbeatVal: Double? // 25.2 - var nickname: String? // "Tina" - var headImg: String? // 头像 URL - var isShow: Bool? -} diff --git a/crush/Crush/Src/Modules/Friend/Model/HeartBeatLevelRelation.swift b/crush/Crush/Src/Modules/Friend/Model/HeartBeatLevelRelation.swift deleted file mode 100644 index 5f33614..0000000 --- a/crush/Crush/Src/Modules/Friend/Model/HeartBeatLevelRelation.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// HeartBeatLevelRelation.swift -// Crush -// -// Created by Leon on 2025/8/25. -// - -struct HeartbeatLeveLDict: Codable{ - var code: String? - var name: String? - var imgUrl: String? - var isUnlock: Bool? - var startVal: Double? -} - -/// 获取心动等级,接口返回的模型! -struct HeartBeatLevelRelation: Codable{ - var aiUserHeartbeatRelation: AIUserHeartBeatRelation? - var heartbeatLeveLDictList: [HeartbeatLeveLDict]? -} diff --git a/crush/Crush/Src/Modules/Friend/Model/NoticeModels.swift b/crush/Crush/Src/Modules/Friend/Model/NoticeModels.swift deleted file mode 100644 index 41561ac..0000000 --- a/crush/Crush/Src/Modules/Friend/Model/NoticeModels.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// NoticeModels.swift -// Crush -// -// Created by Leon on 2025/8/28. -// - -struct MessageStat: Codable { - var unRead: Int? - var latestContent: String? - /// 时间戳 - var latestTime: Int? -} - -struct MessageNotice: Codable { - var id: Int? // 消息ID - var sendUserId: Int? // 发送人用户ID - var type: MessageType? // 消息类型 - var bizId: String? // 业务ID - var status: Int? // 消息状态(0未读、1已读) - var title: String? // 消息标题 - var content: String? // 消息内容 - /// "{\"aiId\":439059452002305}" - var extras: String? // 消息扩展内容 - var replaceJson: String? // 国际化翻译替换数据 - var createTime: Int? - - func getExtra() -> MessageNoticeExtra?{ - guard let extrasString = extras, extrasString.count > 0 else{ - return nil - } - return CodableHelper.decode(MessageNoticeExtra.self, from: extrasString) - } -} - -struct MessageNoticeExtra: Codable{ - var aiId: Int? -} - -enum MessageType: Int, Codable{ - case loginWelcome = 100 - case aiwelcomeGreeting = 101 - case vipRenewSuccess = 102 - case vipRenewFail = 103 - case myAiGifted = 104 - case myAiAlbumPhotoUnlock = 105 - case heartBeatLevelDown = 106 - - func hasNoCheckEntrance()-> Bool{ - switch self{ - case .loginWelcome, .aiwelcomeGreeting: - return true - default: - return false - } - } -} diff --git a/crush/Crush/Src/Modules/Friend/NoticeCenter/NoticeCenterListController.swift b/crush/Crush/Src/Modules/Friend/NoticeCenter/NoticeCenterListController.swift deleted file mode 100644 index 964bb85..0000000 --- a/crush/Crush/Src/Modules/Friend/NoticeCenter/NoticeCenterListController.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// NoticeCenterListController.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import UIKit - -class NoticeCenterListController: CLBaseTableController { - var titleView: TitleView! - override func viewDidLoad() { - super.viewDidLoad() - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - titleView = { - let v = TitleView() - return v - }() - let title = "Notice" - titleView.title = title - titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: titleView.preCalculateHeight()) - tableView.tableHeaderView = titleView - navigationView.alpha0Title = title - - tableView.snp.remakeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - tableView.register(NoticeCenterListCell.self, forCellReuseIdentifier: "NoticeCenterListCell") - - addRefreshHeaderFooter() - } - - private func setupDatas() { - tableView.mj_header?.beginRefreshing() - } - - private func setupEvents() { - } - - override func loadData() { - let pageData = RequestPageData() - pageData.pn = page - - let pageParams = pageData.toNonNilDictionary() - var params = [String: Any]() - params.updateValue(pageParams, forKey: "page") - - IMProvider.request(.messageList(params: params), modelType: ResponseContentPageData.self) { [weak self] result in - self?.tableView.mj_header?.endRefreshing() - self?.tableView.mj_footer?.endRefreshing() - switch result { - case let .success(model): - if let messages = model?.datas { - if pageData.pn == 1 { - self?.datas = messages - self?.tableView.mj_footer?.resetNoMoreData() - self?.view.setupEmpty(empty: messages.count <= 0, msg: "暂无内容") - } else { - self?.datas.append(contentsOf: messages) - if messages.count <= 0 { - self?.tableView.mj_footer?.endRefreshingWithNoMoreData() - } - } - self?.tableView.reloadData() - } - case .failure: - break - } - } - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return datas.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "NoticeCenterListCell", for: indexPath) as! NoticeCenterListCell - if let data = datas[indexPath.row] as? MessageNotice { - cell.config(data) - } - cell.delegate = self - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - } - - override func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} - -extension NoticeCenterListController: NoticeCenterListCellDelegate{ - func noticeCellTapCheck(data:MessageNotice){ - guard let type = data.type else{return} - - switch type { - case .vipRenewSuccess: - // 跳转会员中心 - AppRouter.goVIPCenter() - break - case .vipRenewFail: - // 跳转会员中心 - AppRouter.goVIPCenter() - break - case .myAiGifted: - // 跳转钱包流水 -// Hud.toast(str: "钱包流水") - AppRouter.goBillList() - break - case .myAiAlbumPhotoUnlock: - // 跳转钱包流水 -// Hud.toast(str: "钱包流水") - AppRouter.goBillList() - break - case .heartBeatLevelDown: - // 跳转与角色聊天页面 - guard let model = data.getExtra(), let aiId = model.aiId else{ - assert(false) - return - } - AppRouter.goChatVC(aiId: aiId) - break - default: - break - } - } -} diff --git a/crush/Crush/Src/Modules/Friend/NoticeCenter/View/NoticeCenterListCell.swift b/crush/Crush/Src/Modules/Friend/NoticeCenter/View/NoticeCenterListCell.swift deleted file mode 100644 index 40bfff8..0000000 --- a/crush/Crush/Src/Modules/Friend/NoticeCenter/View/NoticeCenterListCell.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// NoticeCenterListCell.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import UIKit - -protocol NoticeCenterListCellDelegate: AnyObject{ - func noticeCellTapCheck(data:MessageNotice) -} - -class NoticeCenterListCell: UITableViewCell { - weak var delegate: NoticeCenterListCellDelegate? - - var block: UIView! - var contentStackV: UIStackView! - var titleLabel: UILabel! - var contentLabel: LineSpaceLabel! - - var bottomContainer: UIView! - var timeLabel: UILabel! - var checkButton: StyleButton! - - var data: MessageNotice? - 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 - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.backgroundColor = .c.csbn - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 24, bottom: 4, right: 24)) - } - return v - }() - contentStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 8 - v.alignment = .leading - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.numberOfLines = 0 - v.textColor = .text - v.font = .t.tts - contentStackV.addArrangedSubview(v) - return v - }() - - contentLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbs) - v.config(typo) - v.textColor = .text - contentStackV.addArrangedSubview(v) - return v - }() - - bottomContainer = { - let v = UIView() - contentStackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.height.equalTo(32) - } - return v - }() - - timeLabel = { - let v = UILabel() - v.textColor = .c.ctsn - v.font = .t.tbs - bottomContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview() - } - return v - }() - - checkButton = { - let v = StyleButton() - v.tertiary(size: .small) - v.addTarget(self, action: #selector(tapCheckButton), for: .touchUpInside) - bottomContainer.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - checkButton.setTitle("Check", for: .normal) - -// #warning("test") -// testData() - } - - private func testData() { - titleLabel.text = "About CrushLevel AI" - contentLabel.text = "Experience a love story with Crushlevel AI: From \"Hello\" to \"I do\", every conversation is filled with heart.\nAt Crushlevel AI, every\nconversation is writing your love epic—from the awkward \"Hello\" to the trembling \"Marry Me\".\nThose love words that dare not say in reality, unstoppable responses, and fear of injury have finally found a place to be placed." - timeLabel.text = "00:15" - } - - func config(_ data: MessageNotice?){ - self.data = data - guard let msg = data else{ - return - } - - titleLabel.text = msg.title - contentLabel.text = msg.content - - if let timestamp = msg.createTime{ - let display = Date.timerStyle(style: .IMCHAT, millisecond: timestamp) - timeLabel.text = display - }else{ - timeLabel.text = "-" - } - - if let type = data?.type{ - checkButton.isHidden = type.hasNoCheckEntrance() - }else{ - checkButton.isHidden = true - } - } - - @objc private func tapCheckButton() { - delegate?.noticeCellTapCheck(data: self.data!) - } -} diff --git a/crush/Crush/Src/Modules/Friend/Search/ChatSearchListController.swift b/crush/Crush/Src/Modules/Friend/Search/ChatSearchListController.swift deleted file mode 100644 index d0942ad..0000000 --- a/crush/Crush/Src/Modules/Friend/Search/ChatSearchListController.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// ChatSearchListController.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import JXSegmentedView -import UIKit - -class ChatSearchListController: CLBaseTableController { - weak var viewModel : FriendsSearchViewModel! - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - navigationView.isHidden = true - - tableView.register(SessionListCell.self, forCellReuseIdentifier: "SessionListCell") - - // 添加空状态视图 - view.setupEmpty(empty: true,startY: 100, msg: "No result Yet") - } - - private func setupDats() { - // 初始化时加载所有会话 - loadData() - } - - override func loadData() { - guard let viewModel = viewModel else { return } - - viewModel.searchConversations(text: nil) { [weak self] result, conversations in - DispatchQueue.main.async { - if result { - self?.view.setupEmpty(empty: conversations.count <= 0,startY: 100, msg: "No result Yet") - self?.tableView.reloadData() - } else { - self?.view.setupEmpty(empty: true,startY: 100, msg: "No result Yet") - } - } - } - } - - /// 执行搜索 - func performSearch(with text: String) { - guard let viewModel = viewModel else { return } - - viewModel.searchingStr = text - viewModel.searchConversations(text: text) { [weak self] result, conversations in - DispatchQueue.main.async { - if result { - self?.view.setupEmpty(empty: conversations.count <= 0,startY: 100, msg: "No result Yet") - self?.tableView.reloadData() - } else { - self?.view.setupEmpty(empty: true,startY: 100, msg: "No result Yet") - } - } - } - } - - private func setupEvents() { - } - - // MARK: - Override - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return viewModel?.searchConversationResultCount ?? 0 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "SessionListCell", for: indexPath) as! SessionListCell - - if let viewModel = viewModel, - indexPath.row < viewModel.searchConversations.count { - let conversation = viewModel.searchConversations[indexPath.row] - // 使用支持高亮显示的config方法 - cell.config(conversation, searchText: viewModel.searchingStr.isEmpty ? nil : viewModel.searchingStr) - } - - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let viewModel = viewModel, - indexPath.row < viewModel.searchConversations.count else { - return - } - - let conversation = viewModel.searchConversations[indexPath.row] - let conversationId = conversation.conversationId - AppRouter.goChatVC(conversationId: conversationId, complete: nil) - } -} - -extension ChatSearchListController: JXSegmentedListContainerViewListDelegate { - func listView() -> UIView { - return view - } -} diff --git a/crush/Crush/Src/Modules/Friend/Search/FriendMainSearchController.swift b/crush/Crush/Src/Modules/Friend/Search/FriendMainSearchController.swift deleted file mode 100644 index 41ca62c..0000000 --- a/crush/Crush/Src/Modules/Friend/Search/FriendMainSearchController.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// FriendMainSearchController.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import UIKit - -class FriendMainSearchController: CLViewController { - var viewModel = FriendsSearchViewModel() - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - container.searchField.becomeFirstResponder() - } - - private func setupViews() { - navigationView.backButton.isHidden = true - navigationView.isHidden = true - } - - private func setupDats() { - container.messageListVc.viewModel = viewModel - container.friendsListVc.viewModel = viewModel - } - - private func setupEvents() { - container.cancelButton.addTarget(self, action: #selector(tapCancel), for: .touchUpInside) - } - - @objc private func tapCancel() { - close() - } -} diff --git a/crush/Crush/Src/Modules/Friend/Search/FriendSearchListController.swift b/crush/Crush/Src/Modules/Friend/Search/FriendSearchListController.swift deleted file mode 100644 index a1b9c20..0000000 --- a/crush/Crush/Src/Modules/Friend/Search/FriendSearchListController.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// FriendSearchListController.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import JXSegmentedView -import UIKit - -class FriendSearchListController: CLBaseTableController { - weak var viewModel: FriendsSearchViewModel! - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - navigationView.isHidden = true - - tableView.register(FriendsListCell.self, forCellReuseIdentifier: "FriendsListCell") - - // 添加空状态视图 - view.setupEmpty(empty: true,startY: 100, msg: "No result Yet") - } - - private func setupDats() { - // 初始化时加载所有好友 - loadData() - } - - override func loadData() { - guard let viewModel = viewModel else { return } - - viewModel.searchFriends(page: 1, text: nil) { [weak self] result, friends in - DispatchQueue.main.async { - if result { - self?.view.setupEmpty(empty: friends.count <= 0,startY: 100, msg: "No result Yet") - self?.tableView.reloadData() - } else { - self?.view.setupEmpty(empty: true, msg: "No result Yet") - } - } - } - } - - /// 执行搜索 - func performSearch(with text: String) { - guard let viewModel = viewModel else { return } - - viewModel.searchingStr = text - viewModel.searchFriends(page: 1, text: text) { [weak self] result, friends in - DispatchQueue.main.async { - if result { - self?.view.setupEmpty(empty: friends.count <= 0,startY: 100, msg: "No result Yet") - self?.tableView.reloadData() - } else { - self?.view.setupEmpty(empty: true,startY: 100, msg: "No result Yet") - } - } - } - } - - private func setupEvents() { - } - - // MARK: - Override - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return viewModel?.searchResultCount ?? 0 - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "FriendsListCell", for: indexPath) as! FriendsListCell - cell.delegate = self - - if let viewModel = viewModel, - indexPath.row < viewModel.searchFrineds.count { - let friend = viewModel.searchFrineds[indexPath.row] - // 使用支持高亮显示的config方法 - cell.config(friend, searchText: viewModel.searchingStr.isEmpty ? nil : viewModel.searchingStr) - } - - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let viewModel = viewModel, - indexPath.row < viewModel.searchFrineds.count, - let data = viewModel.searchFrineds[indexPath.row] as? RelationFriend, - let aiId = data.aiId else { - return - } - AppRouter.goAIRoleHome(aiId: Int(aiId)) - } -} - -extension FriendSearchListController: FriendsListCellDelegate { - func friendsCellTapChat(aiId: Int) { - AppRouter.goChatVC(aiId: aiId) - } -} - -extension FriendSearchListController: JXSegmentedListContainerViewListDelegate { - func listView() -> UIView { - return view - } -} diff --git a/crush/Crush/Src/Modules/Friend/Search/View/FriendMainSearchView.swift b/crush/Crush/Src/Modules/Friend/Search/View/FriendMainSearchView.swift deleted file mode 100644 index aa158e5..0000000 --- a/crush/Crush/Src/Modules/Friend/Search/View/FriendMainSearchView.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// FriendMainSearchView.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import UIKit -import JXSegmentedView -import JXPagingView - -class FriendMainSearchView: UIView{ - var naviContainer: UIView! - var cancelButton: TextButton! - var searchField: CLTextField! - - private let segmentedViewHeight = 48 - - private lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - private lazy var listContainerView = JXSegmentedListContainerView(dataSource: self) - - private let dataSource = JXSegmentedTitleDataSource() - private let titles = ["Message", "Friends"] - - lazy var messageListVc = ChatSearchListController() - lazy var friendsListVc = FriendSearchListController() - - - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - naviContainer = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(44) - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.statusBarHeight) - } - addSubview(v) - return v - }() - - cancelButton = { - let v = TextButton() - v.text = "Cancel" - naviContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-24) - } - return v - }() - - searchField = { - let v = CLTextField() - v.switchToNaviSearchField() - v.limit.maxCharacterNumber = 50 - v.placeholder = "Search" - naviContainer.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(cancelButton.snp.leading).offset(-24) - make.leading.equalToSuperview().offset(24) - make.centerY.equalToSuperview() - } - return v - }() - - setupJXViews() - } - - private func setupJXViews(){ - dataSource.searchListMainStyle() - dataSource.titles = titles - - segmentedView.searchListMainStyle() - segmentedView.dataSource = dataSource - segmentedView.delegate = self - - addSubview(segmentedView) - segmentedView.snp.makeConstraints { make in - make.height.equalTo(segmentedViewHeight) - make.leading.equalToSuperview() - //make.bottom.equalToSuperview() - make.top.equalTo(naviContainer.snp.bottom).offset(8) - make.trailing.equalToSuperview() - } - - listContainerView.contentScrollView().isScrollEnabled = false - addSubview(listContainerView) - listContainerView.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalTo(segmentedView.snp.bottom).offset(8) - } - - segmentedView.listContainer = listContainerView - } - - private func setupData(){ - - } - - private func setupEvent(){ - // 添加搜索文本变化监听 - searchField.addTarget(self, action: #selector(searchTextChanged), for: .editingChanged) - - // 添加搜索按钮点击监听 - searchField.addTarget(self, action: #selector(searchButtonTapped), for: .editingDidEndOnExit) - } - - @objc private func searchTextChanged() { - guard searchField.text != nil else { return } - - // 实时搜索,延迟执行以避免频繁请求 - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil) - perform(#selector(performSearch), with: nil, afterDelay: 0.5) - } - - @objc private func searchButtonTapped() { - searchField.resignFirstResponder() - performSearch() - } - - @objc private func performSearch() { - guard let searchText = searchField.text else { return } - -// messageListVc.performSearch(with: searchText) -// friendsListVc.performSearch(with: searchText) - - - // 根据当前选中的标签页执行相应的搜索 - let currentIndex = segmentedView.selectedIndex - if currentIndex == 0 { // Message 标签页 - messageListVc.performSearch(with: searchText) - } else if currentIndex == 1 { // Friends 标签页 - friendsListVc.performSearch(with: searchText) - } - } -} - -extension FriendMainSearchView: JXSegmentedListContainerViewDataSource, JXSegmentedViewDelegate{ - - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { - guard let searchText = searchField.text else { return } - if index == 0 { // Message 标签页 - messageListVc.performSearch(with: searchText) - } else if index == 1 { // Friends 标签页 - friendsListVc.performSearch(with: searchText) - } - } - - func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int { - return titles.count - } - - func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate { - if index == 0 { - if let searchText = searchField.text{ - messageListVc.performSearch(with: searchText) - } - return messageListVc - } else { - if let searchText = searchField.text{ - friendsListVc.performSearch(with: searchText) - } - return friendsListVc - } - } - - -} diff --git a/crush/Crush/Src/Modules/Friend/View/FriendsListCell.swift b/crush/Crush/Src/Modules/Friend/View/FriendsListCell.swift deleted file mode 100644 index 076ae32..0000000 --- a/crush/Crush/Src/Modules/Friend/View/FriendsListCell.swift +++ /dev/null @@ -1,297 +0,0 @@ -// -// FrinendsListCell.swift -// Crush -// -// Created by Leon on 2025/8/13. -// - -import UIKit - -protocol FriendsListCellDelegate: AnyObject{ - func friendsCellTapChat(aiId: Int) -} - -class FriendsListCell: UITableViewCell { - weak var delegate : FriendsListCellDelegate? - - var avatarView: CLImageView! - var badge: BadgeView! - - var chatEntrance: UIButton! - - var contentStackV: UIStackView! - - var namesStackH: UIStackView! - var nameLabel: UILabel! - var relationTag: RelationshipTag! - var subContentH: UIStackView! - var heartbeatView: HeartBeatView! - var line: UIView! - var subLabel: UILabel! - - var data: RelationFriend? - - 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 - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - avatarView = { - let v = CLImageView() - contentView.addSubview(v) - let wh = 48.0 - v.layer.cornerRadius = wh * 0.5 - v.layer.masksToBounds = true - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: wh, height: wh)) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - chatEntrance = { - let v = UIButton() - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - v.backgroundColor = .c.cpn - v.contentMode = .center - v.addTarget(self, action: #selector(tapChat), for: .touchUpInside) - let img = MWIconFont.image(fromIcon: .chat, size: CGSize(width: 16, height: 16), color: .white) - v.setImage(img, for: .normal) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 48, height: 32)) - make.trailing.equalToSuperview().offset(-24) - make.centerY.equalToSuperview() - } - return v - }() - - contentStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 4 - v.alignment = .leading - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(avatarView.snp.trailing).offset(12) - make.centerY.equalToSuperview() - make.trailing.lessThanOrEqualTo(chatEntrance.snp.leading).offset(-8) - } - return v - }() - - namesStackH = { - let v = UIStackView() - v.spacing = 4 - v.alignment = .center - contentStackV.addArrangedSubview(v) - return v - }() - - nameLabel = { - let v = UILabel() - v.font = .t.tts - v.textColor = .text - namesStackH.addArrangedSubview(v) - return v - }() - - relationTag = { - let v = RelationshipTag() - namesStackH.addArrangedSubview(v) - return v - }() - - subContentH = { - let v = UIStackView() - contentStackV.addArrangedSubview(v) - v.spacing = 8 - v.alignment = .center - return v - }() - - heartbeatView = { - let v = HeartBeatView(viewSize: .small) - subContentH.addArrangedSubview(v) - return v - }() - - line = { - let v = UIView() - v.backgroundColor = .c.con - subContentH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 1, height: 12)) - } - return v - }() - - subLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tbs - subContentH.addArrangedSubview(v) - return v - }() - } - - private func setupData() { -// #warning("test") -// testData() - } - - private func testData() { - relationTag.type = .friend - nameLabel.text = "Lee" - heartbeatView.content = "37.8℃" - subLabel.text = "27 · Sensual · Romantic" - avatarView.loadImage(UserCore.shared.user?.headImage) - } - - private func setupEvent() { - } - - // MARK: - Public - - func config(_ data: RelationFriend?) { - self.data = data - guard let theData = data else { - return - } - - relationTag.bind(level: theData.heartbeatLevel) - nameLabel.text = theData.nickname - if let heartbeatVal = theData.heartbeatVal { - heartbeatView.isHidden = false - heartbeatView.content = "\(heartbeatVal)" - } else { - heartbeatView.isHidden = true - heartbeatView.content = "" - } - - if let heartBeatLevel = theData.heartbeatLevel , theData.isShow.boolValue{ - relationTag.isHidden = false - relationTag.bind(level: heartBeatLevel) - }else{ - relationTag.isHidden = true - } - - - var showsArray = [String]() - if let birthday = theData.birthday { - let date = Date.dateFromMilliseconds(birthday) - let age = Date().years(from: date) - showsArray.append("\(age)") - } - - if let characterName = theData.characterName { - showsArray.append(characterName) - } - - if let tagName = theData.tagName { - showsArray.append(tagName) - } - - if showsArray.count > 0 { - subLabel.text = showsArray.joined(separator: " · ") - } else { - subLabel.text = "" - } - - avatarView.loadImage(theData.headImg) - // ... - } - - /// 配置好友数据,支持搜索高亮 - /// - Parameters: - /// - data: 好友数据 - /// - searchText: 搜索关键词,用于高亮显示 - func config(_ data: RelationFriend?, searchText: String?) { - self.data = data - guard let theData = data else { - return - } - - relationTag.bind(level: theData.heartbeatLevel) - - // 设置名称,支持高亮显示 - if let searchText = searchText, !searchText.isEmpty, let nickname = theData.nickname { - nameLabel.attributedText = TextHighlightHelper.highlightText(nickname, searchText: searchText) - } else { - nameLabel.text = theData.nickname - } - - if let heartbeatVal = theData.heartbeatVal { - heartbeatView.isHidden = false - heartbeatView.content = "\(heartbeatVal)" - } else { - heartbeatView.isHidden = true - heartbeatView.content = "" - } - - if let heartBeatLevel = theData.heartbeatLevel , theData.isShow.boolValue{ - relationTag.bind(level: heartBeatLevel) - }else{ - relationTag.isHidden = true - } - - - var showsArray = [String]() - if let birthday = theData.birthday { - let date = Date.dateFromMilliseconds(birthday) - let age = Date().years(from: date) - showsArray.append("\(age)") - } - - if let characterName = theData.characterName { - showsArray.append(characterName) - } - - if let tagName = theData.tagName { - showsArray.append(tagName) - } - - if showsArray.count > 0 { - subLabel.text = showsArray.joined(separator: " · ") - } else { - subLabel.text = "" - } - - avatarView.loadImage(theData.headImg) - // ... - } - - // MARK: - Action - - @objc private func tapChat() { - guard let aiId = self.data?.aiId else { - return - } - delegate?.friendsCellTapChat(aiId: Int(aiId)) - } -} diff --git a/crush/Crush/Src/Modules/Friend/View/FriendsNoticeOnceView.swift b/crush/Crush/Src/Modules/Friend/View/FriendsNoticeOnceView.swift deleted file mode 100644 index f0b074d..0000000 --- a/crush/Crush/Src/Modules/Friend/View/FriendsNoticeOnceView.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// FriendsNoticeOnceView.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import UIKit - -class FriendsNoticeOnceView: UIView { - var icon: UIImageView! - var textLabel: LineSpaceLabel! - var closeButton: UIButton! - - var tapCloseAction: (()-> Void)? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.cpn - cornerRadius = 8 - - icon = { - let v = UIImageView() - v.image = UIImage(named: "icon_pink_heart") - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(24) - make.size.equalTo(CGSize(width: 24, height: 24)) - } - return v - }() - - closeButton = { - let v = UIButton() - let image = MWIconFont.image(fromIcon: .delete, size: CGSize(width: 12, height: 12), color: .text) - v.addTarget(self, action: #selector(tapClose), for: .touchUpInside) - v.contentMode = .center - v.setImage(image, for: .normal) - addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 44, height: 44)) - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(0) - } - return v - }() - - textLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tls) - v.config(typo) - v.textColor = .text - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(icon.snp.trailing).offset(16) - make.trailing.equalTo(closeButton.snp.leading).offset(0) - make.top.equalToSuperview().offset(12) - make.bottom.equalToSuperview().offset(-12) - } - return v - }() - } - - private func setupData() { - textLabel.text = "The heartbeat value above 12.0℃ can appear in the relationship list" - } - - private func setupEvent() { - } - - @objc func tapClose() { - AppCache.cache(key: CacheKey.friendsHeartBeatShowedOnce.rawValue, value: true) - tapCloseAction?() - removeFromSuperview() - } -} diff --git a/crush/Crush/Src/Modules/Friend/View/FriendsPiecesViews.swift b/crush/Crush/Src/Modules/Friend/View/FriendsPiecesViews.swift deleted file mode 100644 index d65459a..0000000 --- a/crush/Crush/Src/Modules/Friend/View/FriendsPiecesViews.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// FriendsPiecesViews.swift -// Crush -// -// Created by Leon on 2025/8/14. -// - -import UIKit - -/// 当前 heartbeat 排名 -class FriendsHeartBeatHead: UIView { - - var stackV: UIStackView! - - // Part 1 - var blockView: UIView! - var queryButton: EPIconGhostButton! - var label: LineSpaceLabel! - - // Part 2 - var noticeOnceView: FriendsNoticeOnceView! - - var heightChangeBlock: ((CGFloat) -> Void)? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - stackV = { - let v = UIStackView() - v.axis = .vertical - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(16) - make.leading.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - - blockView = { - let v = UIView() - v.layer.cornerRadius = 8 - v.layer.masksToBounds = true - v.backgroundColor = .c.csbn -// addSubview(v) -// v.snp.makeConstraints { make in -// make.top.equalToSuperview().offset(8) -// make.leading.equalToSuperview().offset(8) -// make.trailing.equalToSuperview().offset(-8) -// } - stackV.addArrangedSubview(v) - return v - }() - - queryButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .small, iconCode: .faq) - v.addTarget(self, action: #selector(tapQueryButton), for: .touchUpInside) - blockView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - label = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tls) - v.config(typo) - v.textColor = .text - blockView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(8) - make.bottom.equalToSuperview().offset(-8) - make.trailing.equalTo(queryButton.snp.leading).offset(8) - } - return v - }() - - noticeOnceView = { - let v = FriendsNoticeOnceView() - stackV.addArrangedSubview(v) - return v - }() - - noticeOnceView.isHidden = true - blockView.isHidden = true - } - - private func setupData() { -//#warning("test") -// testData() - - } - private func testData(){ - label.text = "Sum of heartbeat values: top 10.12%" - } - - private func setupEvent() { - noticeOnceView.tapCloseAction = {[weak self] in - self?.blockView.isHidden = false - } - } - - // MARK: - Public - public func showNoticeOnce(_ show: Bool){ - noticeOnceView.isHidden = !show - blockView.isHidden = show - } - - /// 0.9 -> 90% - public func config(percent:CGFloat?){ - if self.noticeOnceView.isHidden == true{ - self.blockView.isHidden = false - } - - let percentValue = percent ?? 0.0 - - // 限制范围 0 ~ 1,避免传入非法值 - let clamped = min(max(percentValue, 0.0), 1.0) - - // 转换成百分比,保留两位小数(如果是整数则自动不显示多余小数) - let formatter = NumberFormatter() - formatter.numberStyle = .percent - formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 - - let formatPercent = formatter.string(from: NSNumber(value: Double(clamped))) ?? "0%" - - // 更新 UI - label.text = "Sum of heartbeat values: top \(formatPercent)" - } - - // MARK: - Action - @objc private func tapQueryButton() { - let alert = Alert(title: "Tips", text: "与你的心动值达到15.0摄氏度以上的角色作为最为排名对象,按照这些角色心动值总和进行排名") - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) { - - } - alert.addAction(action1) - alert.show() - } - - - override func layoutSubviews() { - super.layoutSubviews() - - let maxY = stackV.frame.maxY + 8 - heightChangeBlock?(maxY) - } -} diff --git a/crush/Crush/Src/Modules/Friend/View/FriendsRootHomeView.swift b/crush/Crush/Src/Modules/Friend/View/FriendsRootHomeView.swift deleted file mode 100644 index c58756f..0000000 --- a/crush/Crush/Src/Modules/Friend/View/FriendsRootHomeView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// FriendsRootHomeView.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -class FriendsRootHomeView: UIView { - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - } -} diff --git a/crush/Crush/Src/Modules/Friend/View/HeartBeatView.swift b/crush/Crush/Src/Modules/Friend/View/HeartBeatView.swift deleted file mode 100644 index f28178e..0000000 --- a/crush/Crush/Src/Modules/Friend/View/HeartBeatView.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// HeartBeatView.swift -// Crush -// -// Created by Leon on 2025/8/14. -// - -import UIKit - -enum HeartBeatSize { - case small - case medium -} - -/// 小爱心❤️ + 37.8温度 -class HeartBeatView: UIView{ - var icon: UIImageView! - var value: UILabel! - - var viewSize: HeartBeatSize = .small - - var content: String = "-"{ - didSet{ - if content.count > 0{ - value.text = "\(content)℃" - }else{ - value.text = "-" - } - } - } - - init(viewSize: HeartBeatSize) { - self.viewSize = viewSize - super.init(frame: .zero) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - icon = { - let v = UIImageView() - v.image = UIImage(named: "heartbeat") - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - value = { - let v = UILabel() - v.textColor = .text - v.setContentCompressionResistancePriority(UILayoutPriority(756), for: .horizontal) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(icon.snp.trailing).offset(4) - make.top.equalToSuperview() - make.bottom.equalToSuperview() - make.trailing.equalToSuperview() - } - return v - }() - value.text = "-" - } - - private func setupData() { - switch viewSize { - case .small: - icon.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 12, height: 12)) - } - value.font = .t.tls - case .medium: - icon.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 20, height: 20)) - } - value.font = .t.tlm - } - } - - private func setupEvent() { - } -} diff --git a/crush/Crush/Src/Modules/Friend/ViewMode/FriendsSearchViewModel.swift b/crush/Crush/Src/Modules/Friend/ViewMode/FriendsSearchViewModel.swift deleted file mode 100644 index 8ea1cbc..0000000 --- a/crush/Crush/Src/Modules/Friend/ViewMode/FriendsSearchViewModel.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// FriendsSearchViewModel.swift -// Crush -// -// Created by Leon on 2025/8/28. -// - -import Foundation -import NIMSDK - -class FriendsSearchViewModel { - var searchingStr : String = "" - - var searchFrineds: [RelationFriend] = [RelationFriend]() - var searchConversations: [V2NIMConversation] = [V2NIMConversation]() - - // 存储所有好友数据,用于本地搜索 - private var allFriends: [RelationFriend] = [] - // 存储所有会话数据,用于本地搜索 - private var allConversations: [V2NIMConversation] = [] - - /// 搜索好友,支持按 nickname 部分匹配 - func searchFriends(page: Int = 1, text: String?, completion: ((_ result: Bool, _ friends: [RelationFriend]) -> Void)?) { - guard let searchText = text, !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - searchFrineds = [] - completion?(true, []) - return - } - - // 如果已有所有好友数据,进行本地搜索 - if !allFriends.isEmpty { - let filteredFriends = allFriends.filter { friend in - guard let nickname = friend.nickname else { return false } - return nickname.lowercased().contains(searchText.lowercased()) - } - searchFrineds = filteredFriends - completion?(true, filteredFriends) - } else { - // 如果没有本地数据,先加载所有好友再搜索 - loadAllFriends(page: page) { [weak self] result, friends in - if result { - let filteredFriends = friends.filter { friend in - guard let nickname = friend.nickname else { return false } - return nickname.lowercased().contains(searchText.lowercased()) - } - self?.searchFrineds = filteredFriends - completion?(true, filteredFriends) - } else { - completion?(false, []) - } - } - } - } - - /// 搜索会话,支持按会话名称部分匹配 - func searchConversations(text: String?, completion: ((_ result: Bool, _ conversations: [V2NIMConversation]) -> Void)?) { - guard let searchText = text, !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - searchConversations = [] - completion?(true, []) - return - } - - // 如果已有所有会话数据,进行本地搜索 - if !allConversations.isEmpty { - let filteredConversations = allConversations.filter { conversation in - guard let name = conversation.name else { return false } - return name.lowercased().contains(searchText.lowercased()) - } - searchConversations = filteredConversations - completion?(true, filteredConversations) - } else { - // 如果没有本地数据,先加载所有会话再搜索 - loadAllConversations { [weak self] result, conversations in - if result { - let filteredConversations = conversations.filter { conversation in - guard let name = conversation.name else { return false } - return name.lowercased().contains(searchText.lowercased()) - } - self?.searchConversations = filteredConversations - completion?(true, filteredConversations) - } else { - completion?(false, []) - } - } - } - } - - /// 加载所有好友数据 - private func loadAllFriends(page: Int = 1, completion: ((_ result: Bool, _ friends: [RelationFriend]) -> Void)?) { - let pageData = RequestPageData() - pageData.pn = page - var request = FriendsListRequest() - request.page = pageData - let params = request.toNonNilDictionary() - - FriendsProvider.request(.heartbeatRelationList(params: params), modelType: ResponseContentPageData.self) { [weak self] result in - switch result { - case let .success(model): - if let validDatas = model?.datas { - if pageData.pn == 1 { - self?.allFriends = validDatas - completion?(true, validDatas) - } else { - self?.allFriends.append(contentsOf: validDatas) - completion?(true, validDatas) - } - } else { - completion?(false, []) - } - case .failure: - completion?(false, []) - } - } - } - - /// 加载所有会话数据 - private func loadAllConversations(completion: ((_ result: Bool, _ conversations: [V2NIMConversation]) -> Void)?) { - guard NIMSDK.shared().v2LoginService.getLoginStatus() == .LOGIN_STATUS_LOGINED else { - completion?(false, []) - return - } - - NIMSDK.shared().v2ConversationService.getConversationList(0, limit: 1000) { [weak self] result in - if let conversations = result.conversationList, conversations.count > 0 { - self?.allConversations = conversations - completion?(true, conversations) - } else { - completion?(false, []) - } - } failure: { error in - dlog("☁️load conversations error: \(error.description)") - completion?(false, []) - } - } - - /// 清除搜索数据 - func clearSearch() { - searchFrineds.removeAll() - searchConversations.removeAll() - searchingStr = "" - } - - /// 获取好友搜索结果数量 - var searchResultCount: Int { - return searchFrineds.count - } - - /// 获取会话搜索结果数量 - var searchConversationResultCount: Int { - return searchConversations.count - } -} - diff --git a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardContainer.swift b/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardContainer.swift deleted file mode 100644 index 5886b8b..0000000 --- a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardContainer.swift +++ /dev/null @@ -1,752 +0,0 @@ -// -// MeetDragCardContainer.swift -// Crush -// -// Created by AI Assistant on 2024/12/19. -// Copyright © 2024年 Crush. All rights reserved. -// - -import UIKit -import SnapKit -import Lottie - -// MARK: - 数据源协议 -protocol MeetDragCardContainerDataSource: AnyObject { - /// 数据源个数 - func numberOfRowsInYFLDragCardContainer(_ container: MeetDragCardContainer) -> Int - - /// 显示数据源 - func container(_ container: MeetDragCardContainer, viewForRowsAt index: Int) -> MeetDragCardView -} - -// MARK: - 代理协议 -protocol MeetDragCardContainerDelegate: AnyObject { - /// 点击卡片回调(⚠️废弃) - func container(_ container: MeetDragCardContainer, didSelectRowAt index: Int) - - /// 拖到最后一张卡片 YES,空,可继续调用reloadData分页数据 - func container(_ container: MeetDragCardContainer, dataSourceIsEmpty isEmpty: Bool) - - /// 当前cardview 是否可以拖拽,默认YES - func container(_ container: MeetDragCardContainer, canDragForCardView cardView: MeetDragCardView) -> Bool - - /// 卡片处于拖拽中回调 - func container(_ container: MeetDragCardContainer, dargingForCardView cardView: MeetDragCardView, direction: ContainerDragDirection, widthRate: CGFloat, heightRate: CGFloat) - - /// 询问这次滑动的方向是否生效,控制 v2.6 ld add - func container(_ container: MeetDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) -> Bool - - /// 卡片拖拽结束回调(卡片消失) - func container(_ container: MeetDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) - - /// 卡片回看 v2.6 添加 - func container(_ container: MeetDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) - - func container(_ container: MeetDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: MeetDragCardView) -} - -// MARK: - 主容器类 -class MeetDragCardContainer: UIView { - - // MARK: - 公开属性 - private(set) var tmpStoreNopeCardView: MeetDragCardView? - private(set) var smallCardMode: Bool = false - private(set) var isMoveIng: Bool = false - - weak var dataSource: MeetDragCardContainerDataSource? - weak var delegate: MeetDragCardContainerDelegate? - - // MARK: - 私有属性 - private var cards: [MeetDragCardView] = [] - private var direction: ContainerDragDirection = .default - private var loadedIndex: Int = 0 - private var firstCardFrame: CGRect = .zero - private var lastCardFrame: CGRect = .zero - private var cardCenter: CGPoint = .zero - private var lastCardTransform: CGAffineTransform = .identity - private var configure: MeetDragConfigure! - - private var likeOrDislikeShowLogoView: UIView! - private var likeShowImageView: UIImageView! - private var dislikeShowImageView: UIImageView! -// private var likeLottieView: LottieAnimationView! -// private var dislikeLottieView: LottieAnimationView! - - - // v2.8 - private var currentIntialGestureDirection: ContainerDragDirection = .default - private var canMoveView: Bool = true - - // MARK: - 初始化方法 - convenience override init(frame: CGRect) { - self.init(frame: frame, configure: MeetDragCardContainer.setDefaultsConfigures()) - } - - init(frame: CGRect, configure: MeetDragConfigure) { - super.init(frame: frame) - initDataConfigure(configure) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - initDataConfigure(MeetDragCardContainer.setDefaultsConfigures()) - } - - // MARK: - 私有方法 - private static func setDefaultsConfigures() -> MeetDragConfigure { - let configure = MeetDragConfigure() - configure.visableCount = 3 - configure.containerEdge = 0//16.0 - configure.cardEdge = 0.01 - configure.cardCornerRadius = 48.0 - configure.cardCornerBorderWidth = 0.0 - configure.cardBordColor = UIColor.clear - configure.cardVTopEdage = 0 - configure.cardVBottomEdage = 0//12 - return configure - } - - private func initDataConfigure(_ configure: MeetDragConfigure) { - resetInitData() - initialLikeDislikesShowViews() - cards = [] - self.configure = configure - } - - private func initialLikeDislikesShowViews() { - likeOrDislikeShowLogoView = UIView() - likeOrDislikeShowLogoView.backgroundColor = UIColor.clear - likeOrDislikeShowLogoView.isUserInteractionEnabled = false - likeOrDislikeShowLogoView.alpha = 0 - likeOrDislikeShowLogoView.layer.zPosition = 10 - insertSubview(likeOrDislikeShowLogoView, at: 0) - likeOrDislikeShowLogoView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - likeShowImageView = { - let v = UIImageView() - v.image = UIImage(named: "meet_big_like") - likeOrDislikeShowLogoView.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-143) - } - v.isHidden = true - return v - }() - - dislikeShowImageView = { - let v = UIImageView() - v.image = UIImage(named: "meet_big_dislike") - likeOrDislikeShowLogoView.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-143) - } - v.isHidden = true - return v - }() - -// likeLottieView = { -// let animation = LottieAnimation.named("meet_right_swipe_like") -// let v = LottieAnimationView(animation: animation) -// -// v.contentMode = .scaleAspectFit -// v.loopMode = .playOnce -// v.backgroundBehavior = .pauseAndRestore -// v.backgroundColor = .clear -// likeOrDislikeShowLogoView.addSubview(v) -// let size = CGSize(width: 200, height: 200) -// v.size = size -// v.snp.makeConstraints { make in -// make.centerX.equalToSuperview() -// make.bottom.equalToSuperview().offset(-143) -// make.size.equalTo(size) -// } -// v.isHidden = true -// return v -// }() -// -// dislikeLottieView = { -// let animation = LottieAnimation.named("meet_left_swipe_dislike") -// let v = LottieAnimationView(animation: animation) -// -// v.contentMode = .scaleAspectFit -// v.loopMode = .playOnce -// v.backgroundBehavior = .pauseAndRestore -// v.backgroundColor = .clear -// likeOrDislikeShowLogoView.addSubview(v) -// let size = CGSize(width: 200, height: 200) -// v.size = size -// v.snp.makeConstraints { make in -// make.centerX.equalToSuperview() -// make.bottom.equalToSuperview().offset(-143) -// make.size.equalTo(size) -// } -// v.isHidden = true -// return v -// }() - - } - - /// 重置数据(为了二次调用reload) - private func resetInitData() { - loadedIndex = 0 - resetCards() - direction = .default - isMoveIng = false - } - - /// 清掉所有card view - private func resetCards() { - for view in cards { - view.removeFromSuperview() - } - cards.removeAll() - } - - /// 添加子视图 - private func addSubViews() { - guard let dataSource = dataSource else { return } - - let sum = dataSource.numberOfRowsInYFLDragCardContainer(self) - let preLoadViewCount = min(sum, configure.visableCount) - - // 预防越界 - if loadedIndex < sum { - // 当手势滑动,加载第四个,最多创建4个。不存在内存warning。(手势停止,滑动的view没消失,需要干掉多创建的+1) - let targetCount = isMoveIng ? preLoadViewCount + 1 : preLoadViewCount - for _ in cards.count..= 3 { - cardView.frame = lastCardFrame - } else { - let frame = cardView.frame - if firstCardFrame.isEmpty { - firstCardFrame = frame - cardCenter = cardView.center - } - } - } - - /// 移动卡片 - private func moveIngStatusChange(_ scale: CGFloat) { - // 如果正在移动,添加第四个 - if !isMoveIng { - isMoveIng = true - addSubViews() - } else { - // 第四个加载完,立马改变没作用在手势上其他cardview的scale - let absScale = min(abs(scale), boundaryRation) - let transFormtxPoor = (secondCardScale - thirdCardScale) / (boundaryRation / absScale) - let frameYPoor: CGFloat = 0 - - for (index, cardView) in cards.enumerated() { - guard index > 0 else { continue } - - switch index { - case 1: - let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + secondCardScale, y: 1) - let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor) - cardView.transform = scaleTransform.concatenating(translateTransform) - case 2: - let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + thirdCardScale, y: 1) - let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor) - cardView.transform = scaleTransform.concatenating(translateTransform) - case 3: - cardView.transform = lastCardTransform - default: - break - } - } - } - } - - // 手势结束 - private func panGesturemMoveFinishOrCancle(_ cardView: MeetDragCardView, direction: ContainerDragDirection, scale: CGFloat, isDisappear: Bool) { - if !isDisappear { // 没有消失,恢复原样 - if currentIntialGestureDirection == .left || currentIntialGestureDirection == .right || currentIntialGestureDirection == .default { - UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.6, options: [.allowUserInteraction, .curveEaseInOut]) { - self.subRestoreCardState() - } completion: { finished in - } - } else { // 其他情况下没有动画 - subRestoreCardState() - } - } else { - // v2.6 - if let delegate = delegate { - if !delegate.container(self, canDragFinishForDirection: direction, forCardView: cardView) { - subRestoreCardState() - return - } - } - - delegate?.container(self, dragDidFinshForDirection: self.direction, forCardView: cardView) - - if direction == .left || direction == .right { - tmpStoreNopeCardView = cardView - } - - let flag = direction == .left ? -1 : 2 - let screenWidth = UIScreen.main.bounds.width - - UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveLinear, .allowUserInteraction]) { - self.isMoveIng = true - cardView.center = CGPoint(x: CGFloat(flag) * screenWidth, y: CGFloat(flag) * screenWidth / CGFloat(scale) + self.cardCenter.y) - } completion: { finished in - self.isMoveIng = false - cardView.removeFromSuperview() - } - - if let index = cards.firstIndex(of: cardView) { - cards.remove(at: index) - } - isMoveIng = false - resetLayoutSubviews() - } - } - - private func subRestoreCardState() { - // 干掉多创建的第四个.重置标量 - if isMoveIng && cards.count > configure.visableCount { - if let lastView = cards.last { - lastView.removeFromSuperview() - cards.removeLast() - loadedIndex = lastView.tag - } - } - isMoveIng = false - resetLayoutSubviews() - } - - // MARK: - 公开方法 - func reloadData() { - guard dataSource != nil else { - assertionFailure("check dataSource and dataSource Methods!") - return - } - - resetInitData() - addSubViews() - resetLayoutSubviews() - } - - func getCurrentShowCardView() -> MeetDragCardView? { - return cards.first - } - - func getCurrentShowCardViewIndex() -> Int { - return cards.first?.tag ?? 0 - } - - /// 手动控制滑动,且不回调代理方法 - func removeCardViewForDirection(_ direction: ContainerDragDirection) { - guard !isMoveIng else { return } - - let screenWidth = UIScreen.main.bounds.width - var cardCenter = CGPoint.zero - var flag = 0 - - guard let currentShowCardView = cards.first else { return } - - switch direction { - case .left: - cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) - flag = -1 - tmpStoreNopeCardView = currentShowCardView - case .right: - cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) - flag = 1 - tmpStoreNopeCardView = currentShowCardView - default: - break - } - - UIView.animate(withDuration: 0.35) { - self.isMoveIng = true - let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) - currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) - currentShowCardView.center = cardCenter - } completion: { finished in - self.isMoveIng = false - currentShowCardView.removeFromSuperview() - if let index = self.cards.firstIndex(of: currentShowCardView) { - self.cards.remove(at: index) - } - self.addSubViews() - self.resetLayoutSubviews() - } - } - - /// app控制滑动,且回调滑动Finish代理 - func removeCardViewWithCallDelegateForDirection(_ direction: ContainerDragDirection) { - guard !isMoveIng else { return } - - let screenWidth = UIScreen.main.bounds.width - var cardCenter = CGPoint.zero - var flag = 0 - - guard let currentShowCardView = cards.first else { return } - - switch direction { - case .left: - cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) - flag = -1 - //tmpStoreNopeCardView = currentShowCardView - case .right: - cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) - flag = 1 - default: - break - } - tmpStoreNopeCardView = currentShowCardView - - UIView.animate(withDuration: 0.35) { - let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) - currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) - currentShowCardView.center = cardCenter - } completion: { finished in - currentShowCardView.removeFromSuperview() - if let index = self.cards.firstIndex(of: currentShowCardView) { - self.cards.remove(at: index) - } - self.addSubViews() - self.resetLayoutSubviews() - } - - delegate?.container(self, dragDidFinshForDirection: direction, forCardView: currentShowCardView) - } - - /// 业务需要: 手动回看 Meet 卡片,将之前存的临时CardView 回看回来 - func addCardView(_ cardView: MeetDragCardView?, fromDirection direction: ContainerDragDirection) { - guard !isMoveIng else { return } - - let targetCardView = cardView ?? tmpStoreNopeCardView - guard let cardView = targetCardView else { return } - - let screenWidth = UIScreen.main.bounds.width - var cardCenter = CGPoint.zero - var flag = 0 - - switch direction { - case .left: - cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) - flag = -1 - //tmpStoreNopeCardView = cardView - case .right: - cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) - flag = 1 - default: - cardCenter = CGPoint(x: self.cardCenter.x, y: self.cardCenter.y) - flag = 0 - break - } - tmpStoreNopeCardView = cardView - - cards.insert(cardView, at: 0) - addSubview(cardView) - - cardView.center = cardCenter - let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) - cardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) - - cardCenter = CGPoint(x: self.cardCenter.x, y: self.cardCenter.y) - - UIView.animate(withDuration: 0.35) { - cardView.transform = .identity - cardView.center = cardCenter - self.isMoveIng = true - } completion: { finished in - self.isMoveIng = false - self.tmpStoreNopeCardView = nil - } - - delegate?.container(self, lookingBack: direction, forCardView: cardView) - } - - func showNopeLogo(_ show: Bool, widthRate: CGFloat) { - if widthRate == 0 { -// if !isLottieAnimationPlaying(dislikeLottieView) { -// UIView.animate(withDuration: 0.25) { -// self.likeOrDislikeShowLogoView.alpha = 0 -// } -// } - UIView.animate(withDuration: 0.5) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - return - } - - if show { -// likeLottieView.isHidden = true -// dislikeLottieView.isHidden = false - likeShowImageView.isHidden = true - dislikeShowImageView.isHidden = false - likeOrDislikeShowLogoView.isHidden = false - likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) - } else { - UIView.animate(withDuration: 0.45) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - } - } - - func showLikeLogo(_ show: Bool, widthRate: CGFloat) { - if widthRate == 0 { -// if !isLottieAnimationPlaying(likeLottieView) { -// UIView.animate(withDuration: 0.25) { -// self.likeOrDislikeShowLogoView.alpha = 0 -// } -// } - UIView.animate(withDuration: 0.5) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - return - } - - if show { -// likeLottieView.isHidden = false -// dislikeLottieView.isHidden = true - likeShowImageView.isHidden = false - dislikeShowImageView.isHidden = true - likeOrDislikeShowLogoView.isHidden = false - likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) - } else { - UIView.animate(withDuration: 0.45) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - } - } - -// func showNopeLottie() { -// likeLottieView.isHidden = true -// dislikeLottieView.isHidden = false -// stopLottieAnimation(dislikeLottieView) -// likeOrDislikeShowLogoView.alpha = 1 -// -// playLottieAnimation(dislikeLottieView) { [weak self] finish in -// if finish { -// self?.likeOrDislikeShowLogoView.alpha = 0 -// } -// } -// } - -// func showLikeLottie() { -// dislikeLottieView.isHidden = true -// -// likeLottieView.isHidden = false -// stopLottieAnimation(likeLottieView) // 🔥 推测方法 -// likeOrDislikeShowLogoView.alpha = 1 -// -// playLottieAnimation(likeLottieView) { [weak self] finish in -// if finish { -// self?.likeOrDislikeShowLogoView.alpha = 0 -// } -// } -// } - - func showNopeLogo(){ - likeShowImageView.isHidden = true - dislikeShowImageView.isHidden = false - likeOrDislikeShowLogoView.alpha = 1 - UIView.animate(withDuration: 0.5) { [self] in - likeOrDislikeShowLogoView.alpha = 0 - } - } - - func showLikeLogo(){ - likeShowImageView.isHidden = false - dislikeShowImageView.isHidden = true - likeOrDislikeShowLogoView.alpha = 1 - UIView.animate(withDuration: 0.5) { [self] in - likeOrDislikeShowLogoView.alpha = 0 - } - } - - func addAdaptForDragCardView(_ cardView: MeetDragCardView) { - let y = configure.containerEdge + configure.cardVTopEdage - let width = frame.size.width - 2 * configure.containerEdge - let height = frame.size.height - 2 * configure.containerEdge - configure.cardVTopEdage - configure.cardVBottomEdage - - cardView.frame = CGRect(x: configure.containerEdge, y: y, width: width, height: height) - cardView.setConfigure(configure) - cardView.YFLDragCardViewLayoutSubviews() - - recordFrame(cardView) - cardView.tag = loadedIndex - - // 添加拖拽手势 - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - cardView.addGestureRecognizer(panGesture) - - // -// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) -// tapGesture.numberOfTapsRequired = 1 -// tapGesture.numberOfTouchesRequired = 1 -// cardView.addGestureRecognizer(tapGesture) - - addSubview(cardView) - sendSubviewToBack(cardView) - } - - func clearTmpStoreNopeCardView() { - tmpStoreNopeCardView?.removeFromSuperview() - tmpStoreNopeCardView = nil - } - - func switchAllCardsToSmallCardMode(_ smallMode: Bool) { - guard smallCardMode != smallMode else { return } - - smallCardMode = smallMode - delegate?.container(self, enterSmallCardMode: smallCardMode, forCardView: getCurrentShowCardView() ?? MeetDragCardView()) - } - - // MARK: - 手势处理 -// @objc private func handleTapGesture(_ tap: UITapGestureRecognizer) { -// delegate?.container(self, didSelectRowAt: tap.view?.tag ?? 0) -// } - - @objc private func handlePanGesture(_ pan: UIPanGestureRecognizer) { - guard let cardView = pan.view as? MeetDragCardView else { return } - - let canEdit = delegate?.container(self, canDragForCardView: cardView) ?? true - - if canEdit { - switch pan.state { - case .began: - let point = pan.translation(in: self) - doVDirectionLogicByPoint(point, forCardView: cardView) - - case .changed: - guard currentIntialGestureDirection != .up && currentIntialGestureDirection != .down else { return } - - if let delegate = delegate { - // 计算横向滑动比例 >0 向右 <0 向左 - let horizionSliderRate = CGFloat((pan.view?.center.x ?? 0) - cardCenter.x) / CGFloat(cardCenter.x) - let verticalSliderRate = CGFloat((pan.view?.center.y ?? 0) - cardCenter.y) / CGFloat(cardCenter.y) - - // 正在滑动,需要创建第四个。 - moveIngStatusChange(horizionSliderRate) - - if currentIntialGestureDirection == .default { - // 再次决定方向 - let point = pan.translation(in: self) - doVDirectionLogicByPoint(point, forCardView: cardView) - } else { // 已经确定了方向,且走到这儿,肯定是左右滑 - // 以自身的左上角为原点;每次移动后,原点都置0;计算的是相对于上一个位置的偏移; - let point = pan.translation(in: self) - cardView.center = CGPoint(x: (pan.view?.center.x ?? 0) + point.x * 0.5, y: pan.view?.center.y ?? 0) - - // 当angle为正值时,逆时针旋转坐标系统,反之顺时针旋转坐标系统 - let rotationAngle = ((pan.view?.center.x ?? 0) - cardCenter.x) / cardCenter.x * (.pi / 4 / 12) - cardView.transform = cardView.originTransForm.rotated(by: rotationAngle) - pan.setTranslation(.zero, in: self) // 设置坐标原点位上次的坐标 - } - - if horizionSliderRate > 0 { - direction = .right - } else if horizionSliderRate < 0 { - direction = .left - } else { - direction = .default - } - - if currentIntialGestureDirection == .default { - currentIntialGestureDirection = direction - } - - delegate.container(self, dargingForCardView: cardView, direction: direction, widthRate: horizionSliderRate, heightRate: verticalSliderRate) - } - - case .ended, .cancelled: - // 还原card位置,或者消失card - let horizionSliderRate = CGFloat((pan.view?.center.x ?? 0) - cardCenter.x) / CGFloat(cardCenter.x) - let moveY = (pan.view?.center.y ?? 0) - cardCenter.y - let moveX = (pan.view?.center.x ?? 0) - cardCenter.x - - panGesturemMoveFinishOrCancle(cardView, direction: direction, scale: CGFloat(moveX / moveY), isDisappear: abs(horizionSliderRate) > boundaryRation) - - for per in cards { // 上下滑完后,不隐藏后面的cardView - per.isHidden = false - } - - currentIntialGestureDirection = .default - - default: - break - } - } - } - - // MARK: - 辅助方法 - private func doVDirectionLogicByPoint(_ point: CGPoint, forCardView cardView: MeetDragCardView) { - // option2: 不单独处理竖直方向手势 - if point.x == 0 { - currentIntialGestureDirection = .default - } else if point.x < 0 { - currentIntialGestureDirection = .left - } else { - currentIntialGestureDirection = .right - } - } - - private func isLottieAnimationPlaying(_ view: LottieAnimationView) -> Bool { - return view.isAnimationPlaying - } - - private func stopLottieAnimation(_ view: LottieAnimationView) { - view.stop() - } - - private func playLottieAnimation(_ view: LottieAnimationView, completion: @escaping (Bool) -> Void) { - view.play { completed in - completion(completed) - } - } -} diff --git a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardExample.swift b/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardExample.swift deleted file mode 100644 index 2a98a9c..0000000 --- a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardExample.swift +++ /dev/null @@ -1,287 +0,0 @@ -// -// YFLDragCardExample.swift -// Crush -// -// Created by AI Assistant on 2024/12/19. -// Copyright © 2024年 Crush. All rights reserved. -// - -import UIKit -import SnapKit - -// MARK: - 使用示例 -class MeetDragCardExampleViewController: UIViewController { - - private var dragCardContainer: MeetDragCardContainer! - private var dataArray: [String] = ["卡片1", "卡片2", "卡片3", "卡片4", "卡片5"] - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupDragCardContainer() - } - - private func setupUI() { - view.backgroundColor = UIColor.white - - // 创建拖拽卡片容器 - let lr = 12.0 - let cardWidth = UIScreen.width - lr * 2.0 - let cardHeight = 600.0 // 400 - dragCardContainer = MeetDragCardContainer(frame: CGRect(x: 0, y: 0, width: cardWidth, height: cardHeight)) - view.addSubview(dragCardContainer) - - dragCardContainer.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.equalTo(cardWidth) - make.height.equalTo(cardHeight) - } - - // 设置数据源和代理 - dragCardContainer.dataSource = self - dragCardContainer.delegate = self - - // 添加控制按钮 - setupControlButtons() - } - - private func setupControlButtons() { - let leftButton = UIButton(type: .system) - leftButton.setTitle("左滑", for: .normal) - leftButton.backgroundColor = UIColor.red - leftButton.setTitleColor(.white, for: .normal) - leftButton.addTarget(self, action: #selector(leftButtonTapped), for: .touchUpInside) - view.addSubview(leftButton) - - let rightButton = UIButton(type: .system) - rightButton.setTitle("右滑", for: .normal) - rightButton.backgroundColor = UIColor.green - rightButton.setTitleColor(.white, for: .normal) - rightButton.addTarget(self, action: #selector(rightButtonTapped), for: .touchUpInside) - view.addSubview(rightButton) - - let reloadButton = UIButton(type: .system) - reloadButton.setTitle("重新加载", for: .normal) - reloadButton.backgroundColor = UIColor.blue - reloadButton.setTitleColor(.white, for: .normal) - reloadButton.addTarget(self, action: #selector(reloadButtonTapped), for: .touchUpInside) - view.addSubview(reloadButton) - - leftButton.snp.makeConstraints { make in - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20) - make.left.equalToSuperview().offset(20) - make.width.equalTo(80) - make.height.equalTo(40) - } - - rightButton.snp.makeConstraints { make in - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20) - make.centerX.equalToSuperview() - make.width.equalTo(80) - make.height.equalTo(40) - } - - reloadButton.snp.makeConstraints { make in - make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20) - make.right.equalToSuperview().offset(-20) - make.width.equalTo(80) - make.height.equalTo(40) - } - } - - private func setupDragCardContainer() { - // 重新加载数据 - dragCardContainer.reloadData() - } - - @objc private func leftButtonTapped() { - dragCardContainer.removeCardViewForDirection(.left) - } - - @objc private func rightButtonTapped() { - dragCardContainer.removeCardViewForDirection(.right) - } - - @objc private func reloadButtonTapped() { - dragCardContainer.reloadData() - } -} - -// MARK: - MeetDragCardContainerDataSource -extension MeetDragCardExampleViewController: MeetDragCardContainerDataSource { - func numberOfRowsInYFLDragCardContainer(_ container: MeetDragCardContainer) -> Int { - return dataArray.count - } - - func container(_ container: MeetDragCardContainer, viewForRowsAt index: Int) -> MeetDragCardView { - let cardView = CustomDragCardView() - cardView.configure(with: dataArray[index]) - return cardView - } -} - -// MARK: - MeetDragCardContainerDelegate -extension MeetDragCardExampleViewController: MeetDragCardContainerDelegate { - func container(_ container: MeetDragCardContainer, didSelectRowAt index: Int) { - print("点击了卡片: \(index)") - } - - func container(_ container: MeetDragCardContainer, dataSourceIsEmpty isEmpty: Bool) { - if isEmpty { - print("数据源为空,可以加载更多数据") - // 这里可以加载更多数据 - loadMoreData() - } - } - - func container(_ container: MeetDragCardContainer, canDragForCardView cardView: MeetDragCardView) -> Bool { - return true - } - - func container(_ container: MeetDragCardContainer, dargingForCardView cardView: MeetDragCardView, direction: ContainerDragDirection, widthRate: CGFloat, heightRate: CGFloat) { - // 拖拽过程中的回调 - if direction == .left { - container.showNopeLogo(true, widthRate: CGFloat(abs(widthRate))) - } else if direction == .right { - container.showLikeLogo(true, widthRate: CGFloat(abs(widthRate))) - } else { - container.showNopeLogo(false, widthRate: 0) - container.showLikeLogo(false, widthRate: 0) - } - } - - func container(_ container: MeetDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) -> Bool { - return true - } - - func container(_ container: MeetDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) { - print("卡片拖拽完成,方向: \(direction)") - - // 显示相应的动画 - switch direction { - case .left: - // container.showNopeLottie() - break - case .right: -// container.showLikeLottie() - break - default: - break - } - } - - func container(_ container: MeetDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) { - print("卡片回看,方向: \(direction)") - } - - func container(_ container: MeetDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: MeetDragCardView) { - print("进入小卡片模式: \(smallCardMode)") - } - - private func loadMoreData() { - // 模拟加载更多数据 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.dataArray.append(contentsOf: ["新卡片1", "新卡片2", "新卡片3"]) - self.dragCardContainer.reloadData() - } - } -} - -// MARK: - 自定义卡片视图 -class CustomDragCardView: MeetDragCardView { - - private let titleLabel = UILabel() - private let contentLabel = UILabel() - private let imageView = UIImageView() - - override init(frame: CGRect) { - super.init(frame: frame) - setupSubviews() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupSubviews() - } - - private func setupSubviews() { - backgroundColor = UIColor.systemBlue - - // 设置圆角和阴影 - layer.cornerRadius = 12 - layer.shadowColor = UIColor.black.cgColor - layer.shadowOffset = CGSize(width: 0, height: 2) - layer.shadowRadius = 4 - layer.shadowOpacity = 0.1 - - // 添加子视图 - addSubview(imageView) - addSubview(titleLabel) - addSubview(contentLabel) - - // 配置子视图 - imageView.backgroundColor = UIColor.lightGray - imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = 8 - imageView.clipsToBounds = true - - titleLabel.font = UIFont.boldSystemFont(ofSize: 18) - titleLabel.textColor = .white - titleLabel.textAlignment = .center - - contentLabel.font = UIFont.systemFont(ofSize: 14) - contentLabel.textColor = .white - contentLabel.textAlignment = .center - contentLabel.numberOfLines = 0 - - // 使用SnapKit进行布局 - imageView.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.centerX.equalToSuperview() - make.width.height.equalTo(100) - } - - titleLabel.snp.makeConstraints { make in - make.top.equalTo(imageView.snp.bottom).offset(20) - make.left.right.equalToSuperview().inset(20) - } - - contentLabel.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(10) - make.left.right.equalToSuperview().inset(20) - make.bottom.lessThanOrEqualToSuperview().offset(-20) - } - } - - func configure(with text: String) { - titleLabel.text = text - contentLabel.text = "这是 \(text) 的详细内容描述" - } - - override func YFLDragCardViewLayoutSubviews() { - super.YFLDragCardViewLayoutSubviews() - // 子类可以在这里进行额外的布局调整 - } - - override func startAnimatingForDirection(_ direction: ContainerDragDirection) { - super.startAnimatingForDirection(direction) - - // 根据方向添加动画效果 - switch direction { - case .left: - // 不喜欢动画 - UIView.animate(withDuration: 0.3) { - self.transform = self.transform.rotated(by: -CGFloat.pi / 6) - self.alpha = 0.7 - } - case .right: - // 喜欢动画 - UIView.animate(withDuration: 0.3) { - self.transform = self.transform.rotated(by: CGFloat.pi / 6) - self.alpha = 0.7 - } - default: - break - } - } -} diff --git a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardView.swift b/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardView.swift deleted file mode 100644 index 667f43f..0000000 --- a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardView.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// MeetDragCardView.swift -// Crush -// -// Created by AI Assistant on 2024/12/19. -// Copyright © 2024年 Crush. All rights reserved. -// - -import UIKit -import SnapKit - -class MeetDragCardView: UIView { - - var originTransForm: CGAffineTransform = .identity - var configure: MeetDragConfigure? - - override init(frame: CGRect) { - super.init(frame: frame) - setUp() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setUp() - } - - private func setUp() { - isUserInteractionEnabled = true - // backgroundColor = UIColor.cyan - } - - func setConfigure(_ configure: MeetDragConfigure) { - self.configure = configure - //设置卡片样式 - layer.cornerRadius = configure.cardCornerRadius - layer.borderWidth = configure.cardCornerBorderWidth - layer.borderColor = configure.cardBordColor.cgColor - layer.masksToBounds = true - } - - /// 布局子视图,其子类重写,并在其进行布局 - func YFLDragCardViewLayoutSubviews() { - // 子类重写此方法进行布局 - } - - /// 执行卡片上动画(喜欢、不喜欢) - func startAnimatingForDirection(_ direction: ContainerDragDirection) { - // 🔥 根据上下文推测:执行动画效果 - switch direction { - case .left: - // 不喜欢动画 - break - case .right: - // 喜欢动画 - break - case .up: - // 向上动画 - break - case .down: - // 向下动画 - break - default: - break - } - } -} diff --git a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragConfigure.swift b/crush/Crush/Src/Modules/Home/CardDrag/MeetDragConfigure.swift deleted file mode 100644 index 50e3f4f..0000000 --- a/crush/Crush/Src/Modules/Home/CardDrag/MeetDragConfigure.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// YFLDragConfigure.swift -// Crush -// -// Created by AI Assistant on 2024/12/19. -// Copyright © 2024年 Crush. All rights reserved. -// - -import UIKit - -// 边界比 -let boundaryRation: CGFloat = 0.2 -let secondCardScale: CGFloat = 1.0//0.95 -let thirdCardScale: CGFloat = 1.0//0.95 - -enum ContainerDragDirection: Int { - case `default` = 0 - case left = 1 - case right = 2 - case up = 3 - case down = 4 -} - -class MeetDragConfigure: NSObject { - var direction: ContainerDragDirection = .default - - /// 可见个数 默认为 3 - var visableCount: Int = 3 - - /// 卡片边距 默认为10.0f - var containerEdge: CGFloat = 10.0 - - /// 卡片内边距 默认为5.0f - var cardEdge: CGFloat = 5.0 - - /// 卡片圆角 默认为10.0f - var cardCornerRadius: CGFloat = 10.0 - - /// 卡片边缘宽度 默认为0.45f - var cardCornerBorderWidth: CGFloat = 0.45 - - /// 卡片边缘颜色 - var cardBordColor: UIColor = UIColor(red: 176.0/255.0, green: 176.0/255.0, blue: 176.0/255.0, alpha: 1.0) - - /// 卡片竖直方向额外的边距,默认0 - var cardVTopEdage: CGFloat = 0 - var cardVBottomEdage: CGFloat = 0 - - override init() { - super.init() - // 设置默认值 - visableCount = 3 - containerEdge = 0// 16.0 - cardEdge = 0.01 - cardCornerRadius = 8.0 - cardCornerBorderWidth = 0.0 - cardBordColor = UIColor.clear - cardVTopEdage = 0 - cardVBottomEdage = 0//12 - } -} diff --git a/crush/Crush/Src/Modules/Home/Guide/HomeMeetGuideController.swift b/crush/Crush/Src/Modules/Home/Guide/HomeMeetGuideController.swift deleted file mode 100644 index c81547e..0000000 --- a/crush/Crush/Src/Modules/Home/Guide/HomeMeetGuideController.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// HomeMeetGuideController.swift -// Crush -// -// Created by Leon on 2025/9/26. -// - -import Lottie -import UIKit - -class HomeMeetGuideController: BaseMaskPopDialogController { - var upGuideContainer: UIView! - var upLottie: LottieAnimationView! - var upDescLabel: UILabel! - - var rightLikeLottie: LottieAnimationView! - var leftLikeLottie: LottieAnimationView! - - weak var playingLottie: LottieAnimationView? - - var stepIndex = 0 - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - private func setupDatas() { - stepIndex = 0 - refreshViews() - } - - private func setupEvents() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) - tapGesture.numberOfTapsRequired = 1 - tapGesture.numberOfTouchesRequired = 1 - block.addGestureRecognizer(tapGesture) - } - - @objc private func handleTapGesture(_ tap: UITapGestureRecognizer) { - // dlog("点击停止") - playingLottie?.stop() - } - - private func refreshViews() { - if stepIndex == 0 { - upGuideContainer.isHidden = false - upLottie.isHidden = false - playingLottie = upLottie - upLottie.play {[weak self] completed in - self?.continueGuide() - } - } else if stepIndex == 1 { - rightLikeLottie.isHidden = false - playingLottie = rightLikeLottie - rightLikeLottie.play{[weak self] completed in - self?.continueGuide() - } - } else if stepIndex == 2 { - leftLikeLottie.isHidden = false - playingLottie = leftLikeLottie - leftLikeLottie.play{[weak self] completed in - self?.continueGuide() - } - } - } - - private func continueGuide(){ - upGuideContainer.isHidden = true - rightLikeLottie.isHidden = true - leftLikeLottie.isHidden = true - - stepIndex += 1 - - if stepIndex > 2 { // 解锁 - dismiss() - AppCache.cache(key: CacheKey.meetGuideSeen.rawValue, value: true) - return - } - - refreshViews() - } - - // MARK: - UI - - private func setupViews() { - navigationView.isHidden = true - closeButton.isHidden = true - block.backgroundColor = .clear - block.snp.remakeConstraints { make in - make.edges.equalToSuperview() - } - - do { - upGuideContainer = { - let v = UIView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - upLottie = { - let animation = LottieAnimation.named("meet_up_swipe_see_more") - let v = LottieAnimationView(animation: animation) - - v.contentMode = .scaleAspectFit - v.loopMode = .loop - v.backgroundBehavior = .pauseAndRestore - v.loopMode = .playOnce - v.backgroundColor = .clear - upGuideContainer.addSubview(v) - let size = CGSize(width: 136, height: 136) - v.size = size - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.centerY.equalToSuperview() - make.size.equalTo(size) - } - v.isHidden = true - return v - }() - - upDescLabel = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .text - upGuideContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(upLottie.snp.bottom).offset(16) - } - return v - }() - - upDescLabel.text = "Swipe to see more" - } - - rightLikeLottie = { - let animation = LottieAnimation.named("meet_right_swipe_like") - let v = LottieAnimationView(animation: animation) - - v.contentMode = .scaleAspectFit - v.loopMode = .loop - v.backgroundBehavior = .pauseAndRestore - v.loopMode = .playOnce - v.backgroundColor = .clear - block.addSubview(v) - let size = CGSize(width: 240, height: 240) - v.size = size - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-120) - make.size.equalTo(size) - } - v.isHidden = true - return v - }() - - leftLikeLottie = { - let animation = LottieAnimation.named("meet_left_swipe_dislike") - let v = LottieAnimationView(animation: animation) - - v.contentMode = .scaleAspectFit - v.loopMode = .loop - v.backgroundBehavior = .pauseAndRestore - v.loopMode = .playOnce - v.backgroundColor = .clear - block.addSubview(v) - let size = CGSize(width: 240, height: 240) - v.size = size - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-120) - make.size.equalTo(size) - } - v.isHidden = true - return v - }() - } -} diff --git a/crush/Crush/Src/Modules/Home/HomePageRootController.swift b/crush/Crush/Src/Modules/Home/HomePageRootController.swift deleted file mode 100644 index 4eb4705..0000000 --- a/crush/Crush/Src/Modules/Home/HomePageRootController.swift +++ /dev/null @@ -1,232 +0,0 @@ -// -// HomePageRootController.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit -import Combine -class HomePageRootController: CLTabRootController { - var navTitleLabel: CLLabel! - var filterButton: EPIconGhostButton! - - var filterConditionCount: Int = 0 - - lazy var viewModel = MeetViewModel() - - @Published var filterModel = RolesFilterModel() - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - //container.viewModel = viewModel - container.setupViewModel(vm: viewModel) - - // Do any additional setup after loading the view. - // view.showEmpty(text: "首页 Coming soon") -// if APIConfig.environment == .test { - #if DEBUG - let debugButton = { - let v = UIButton() - v.setTitle("Debug Entrances", for: .normal) - v.titleLabel?.font = .t.tlm - v.setTitleColor(.yellow, for: .normal) - v.layer.borderColor = UIColor.yellow.cgColor - v.layer.borderWidth = 1 - v.addTarget(self, action: #selector(tapDebugButton), for: .touchUpInside) - navigationView.addSubview(v) - v.snp.makeConstraints { make in -// make.leading.equalToSuperview().offset(24) - make.centerX.equalToSuperview() - make.height.equalTo(30) - make.centerY.equalTo(navigationView.backButton) - } - return v - }() - debugButton.isHidden = false -// } - #endif - - setupViews() - setupDatas() - setupEvents() - - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - } - - private func setupViews() { - navigationView.bgView.alpha = 0 - - navTitleLabel = { - let v = CLLabel() - v.font = .t.tdm - navigationView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(navigationView.titleLabel) - make.leading.equalToSuperview().offset(24) - } - return v - }() - - filterButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .filterFill) - v.addTarget(self, action: #selector(tapFilter), for: .touchUpInside) - v.setBackgroundImage(nil, for: .highlighted) - navigationView.rightStackH.addArrangedSubview(v) - navigationView.paddingRightForRightStack = 24 - v.snp.makeConstraints { _ in - // make.size.equalTo(v.bgImageSize()) - } - return v - }() - filterButton.titleLabel?.font = .t.tll - filterButton.setTitleColor(.text, for: .normal) - filterButton.touchAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - filterButton.layer.masksToBounds = false - - navTitleLabel.text = "Encounter" - } - - private func setupDatas() { - setupOrResetFilterModel() - - // viewModel.loadCards() - loadFirstCards() - } - - private func setupOrResetFilterModel(loadNewData: Bool = false){ - filterModel = RolesFilterModel() - if !UserCore.shared.isLogin(){ - filterModel.sex = [.female] - filterModel.age = .AGE_1 - } - - viewModel.filterModel = filterModel - - if loadNewData{ - loadFirstCards() - } - } - - private func tryShowMeetGuide(){ - guard isDisplaying else{ - return - } - - if AppCache.fetchCache(key: CacheKey.meetGuideSeen.rawValue, type: Bool.self) ?? false == false{ - let vc = HomeMeetGuideController() - vc.show() - } - } - - private func loadFirstCards(){ - Hud.showIndicator() - viewModel.loadCards {[weak self] _, cards in - if cards.count > 0{ - self?.tryShowMeetGuide() - } - Hud.hideIndicator() - } - } - - private func setupEvents() { - $filterModel.sink {[weak self] model in - self?.setupFilterCount(model.filterCount()) - }.store(in: &cancellables) - - NotificationCenter.default.addObserver(self, selector: #selector(notifyLogin), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyLogout), name: AppNotificationName.userLogout.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyNetworkRestore), name: AppNotificationName.networkRestored.notificationName, object: nil) - } - - // MARK: - Helper - - func setupFilterCount(_ count: Int) { - filterConditionCount = count - if count > 0 { - filterButton.optionalIconColor = .text - filterButton.setTitle(" (\(count))", for: .normal) - } else { - filterButton.optionalIconColor = .text - filterButton.setTitle("", for: .normal) - } - filterButton.layoutIfNeeded() - } - - // MARK: - Function - - private func showSecretAdmirerAlert(){ - let content = "Someone is secretly in love with you\n" - let alert = Alert(title: "Secret Admirer", text: content, image: UIImage(named: "heart_meet_48")!) - - alert.masTopToImageBgForImageView?.update(offset: 42) - alert.setupContentStackSpacing(14) - alert.setupBottomBlurImage(image: UIImage.egpic ) - - let aString = NSAttributedString.getIconTitleAttributeByWords(words: "20 unlock", iconImage: UIImage.icon32Diamond, iconSize: CGSize(width: 24, height: 24), textFont: .t.tll, textColor: .white) - let confirmCoin = AlertAction(attributedTitle: aString, actionStyle: .confirm) { - dlog("tap confirm coin") - } - - let cancel = AlertAction(title: "Cancel", actionStyle: .cancel, block: nil) -// alert.addAction(confirm) - alert.addAction(confirmCoin) - alert.addAction(cancel) - alert.show() - alert.layoutIfNeeded() - } - - func setupFilter(filter: RolesFilterModel){ - viewModel.filterModel = filter - Hud.showIndicator() - viewModel.loadCards { _, _ in - Hud.hideIndicator() - } - } - - // MARK: - Action - - @objc private func tapFilter() { -// #warning("test") -// showMatched() -// showSecretAdmirerAlert() - let vc = RolesFilterController() - vc.filterType = .meet - vc.rolesFilterModel = filterModel - presentNaviRootVc(vc: vc) - vc.completion = {[weak self] model in - self?.filterModel = model - // dlog("完成fitler选择:\(model)") - self?.setupFilter(filter: model) - } - } - - @objc private func tapDebugButton() { - UIWindow.getTopViewController()?.navigationController?.pushViewController(TestEntrancesController(), animated: true) - -// #warning("test") -// container.testFunction() - } - - // MARK: - Notification - - @objc private func notifyLogin(){ - setupOrResetFilterModel(loadNewData: true) - } - - @objc private func notifyLogout(){ - setupOrResetFilterModel(loadNewData: true) - } - - @objc private func notifyNetworkRestore(){ - if viewModel.cards.count == 0{ - loadFirstCards() - } - } -} diff --git a/crush/Crush/Src/Modules/Home/Match/MeetMatchedController.swift b/crush/Crush/Src/Modules/Home/Match/MeetMatchedController.swift deleted file mode 100644 index f2d9969..0000000 --- a/crush/Crush/Src/Modules/Home/Match/MeetMatchedController.swift +++ /dev/null @@ -1,417 +0,0 @@ -// -// MeetMatchedController.swift -// Crush -// -// Created by Leon on 2025/9/10. -// - -import UIKit - -class MeetMatchedController: CLBaseViewController { - var bgImageView: CLImageView! - var effectView: UIVisualEffectView! - - var titleWordsContainer: UIView! - var nameAndAgeLabel: CLLabel! - var likeAndCountLabel: CLIconLabel! - - var bottomButtonsStackV: UIStackView! - var chatButton: StyleButton! - var cancelButton: StyleButton! - - // -- 中间部分 - var middleContainer: UIView! - var middleStackV: UIStackView! - // - Avatar - var avatarsContainer: UIView! - var avatarsStackH: UIStackView! - var leftAvatar: CLImageView! - var rightAvatar: CLImageView! - var heartIcon: UIImageView! - // - Gift (可选) - var giftIcon: UIImageView! - // 赠送的礼物数量(可选 - var giftCountBadge: BadgeView! - // - Matched - var matchedContainer: UIView! - var matchedLabel: ColorLabel! - // - Desc - var descLabel: CLLabel! - - // - Data - var card: MeetCard? - var info: MeetMatchInfo? - - // 可选 - var gift: GiftDictModel? - // 礼物数量 - var giftCount: Int = 0 - - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - navigationView.backButton.isHidden = true - navigationView.bgView.alpha = 0 - - bgImageView = { - let v = CLImageView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - v.alpha = 1 - view.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - titleWordsContainer = { - let v = UIView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - make.height.equalTo(60) - } - return v - }() - - nameAndAgeLabel = { - let v = CLLabel() - v.font = .t.ttm - v.textAlignment = .center - titleWordsContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.top.equalToSuperview() - } - return v - }() - - likeAndCountLabel = { - let v = CLIconLabel() - v.iconSize = CGSize(width: 20, height: 20) - v.iconImageView.image = MWIconFont.image(fromIcon: .like, size: CGSize(width: 20, height: 20), color: .white) - v.contentLabel.font = .t.tnds - v.contentLabel.text = "-" - titleWordsContainer.addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview() - make.centerX.equalToSuperview() - } - return v - }() - - bottomButtonsStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 24 - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(48) - make.trailing.equalToSuperview().offset(-48) - make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom * 0.5) - } - return v - }() - - chatButton = { - let v = StyleButton() - v.primary(size: .large) - v.addTarget(self, action: #selector(chatButtonAction), for: .touchUpInside) - bottomButtonsStackV.addArrangedSubview(v) - return v - }() - - cancelButton = { - let v = StyleButton() - v.tertiary(size: .large) - v.addTarget(self, action: #selector(cancelButtonAction), for: .touchUpInside) - bottomButtonsStackV.addArrangedSubview(v) - return v - }() - - middleContainer = { - let v = UIView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(titleWordsContainer.snp.bottom) - make.bottom.equalTo(bottomButtonsStackV.snp.top).offset(0) - } - return v - }() - - middleStackV = { - let v = UIStackView() - v.axis = .vertical - v.alignment = .center - middleContainer.addSubview(v) - v.snp.makeConstraints { make in - //make.centerX.equalToSuperview() - make.leading.equalToSuperview().offset(48) - make.trailing.equalToSuperview().offset(-48) - make.centerY.equalToSuperview() - } - return v - }() - - avatarsContainer = { - let v = UIView() - middleStackV.addArrangedSubview(v) -// v.snp.makeConstraints { make in -// make.height.equalTo(128) -// } - return v - }() - - avatarsStackH = { - let v = UIStackView() - v.axis = .horizontal - v.spacing = 16 - avatarsContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.height.equalTo(128) - } - return v - }() - - leftAvatar = { - let v = CLImageView() - v.cornerRadius = 64 - avatarsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.width.equalTo(128) - make.height.equalTo(128) - } - return v - }() - - rightAvatar = { - let v = CLImageView() - v.cornerRadius = 64 - avatarsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.width.equalTo(128) - make.height.equalTo(128) - } - return v - }() - - heartIcon = { - let v = UIImageView() - v.image = UIImage(named: "chat_level_heart") - avatarsContainer.addSubview(v) - v.snp.makeConstraints { make in -// make.centerX.equalToSuperview() - make.center.equalToSuperview() - make.size.equalTo(CGSize(width:60, height: 60)) - } - return v - }() - - giftIcon = { - let v = UIImageView() - v.image = UIImage(named: "chat_level_gift") - middleStackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width:72, height: 72)) - } - return v - }() - - giftCountBadge = { - let v = BadgeView() - giftIcon.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.top.equalToSuperview().offset(4) - } - return v - }() - - matchedContainer = { - let v = UIView() - middleStackV.addArrangedSubview(v) - return v - }() - - matchedLabel = { - let v = ColorLabel() - v.applyGradient(.theme) - v.font = .t.tdxl - v.textColor = .white - matchedContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(24) - make.bottom.equalToSuperview() - } - return v - }() - - descLabel = { - let v = CLLabel() - v.font = .t.tbm - v.textAlignment = .center - v.textColor = .white - v.numberOfLines = 2 - middleStackV.addArrangedSubview(v) - return v - }() - -// view.backgroundColor = .clear - - chatButton.setTitle("Chat", for: .normal) - cancelButton.setTitle("Cancel", for: .normal) - matchedLabel.text = "Matched" - -// #warning("test") -// testData() - } - - private func testData(){ - nameAndAgeLabel.text = "Angelique, 28" - giftCountBadge.badgeValue = 99 - descLabel.text = "Thank you for your 99 gift names" - likeAndCountLabel.contentLabel.text = "1.2k" - } - - private func setupDatas() { - - rightAvatar.loadImage(UserCore.shared.user?.headImage) - - // 配置数据 - configCardData() - configGiftData() - } - - private func setupEvents() { - } - - // MARK: - Data Configuration - - /// 配置卡片数据 - private func configCardData() { - - if let data = self.card { - // 配置姓名和年龄 - if let nickname = data.nickname, let age = data.age { - nameAndAgeLabel.text = "\(nickname), \(age)" - - } else if let nickname = data.nickname { - nameAndAgeLabel.text = nickname - } - - descLabel.text = "You and \(data.nickname ?? "-") are moved by each other" - - // 配置点赞数 - if let likedCount = data.likedCount { - let displayCount = String.displayNumber(NSNumber(value: likedCount), scale: 1) - likeAndCountLabel.contentLabel.text = displayCount - } - - bgImageView.loadImage(data.imageUrl) - leftAvatar.loadImage(data.headImg) - } - else if let data = info{ - if let nickname = data.nickname, let birthday = data.birthday { - let date = Date.dateFromMilliseconds(Int64(birthday)) - let ageYears = Date().years(from: date) - nameAndAgeLabel.text = "\(nickname), \(ageYears)" - - } else if let nickname = data.nickname { - nameAndAgeLabel.text = nickname - } - - descLabel.text = "You and \(data.nickname ?? "-") are moved by each other" - - // 配置点赞数 - if let likedCount = data.likedCount { - let displayCount = String.displayNumber(NSNumber(value: likedCount), scale: 1) - likeAndCountLabel.contentLabel.text = displayCount - } - - bgImageView.loadImage(data.homeImageUrl) - leftAvatar.loadImage(data.headImg) - } - } - - /// 配置礼物数据 - private func configGiftData() { - guard let gift = gift else { - // 如果没有礼物,隐藏礼物相关UI - giftIcon.isHidden = true - giftCountBadge.isHidden = true - return - } - - // 显示礼物图标 - giftIcon.isHidden = false - giftIcon.loadImage(gift.icon, bgColor: .clear) - - // 配置礼物数量徽章 - if giftCount > 0 { - giftCountBadge.isHidden = false - giftCountBadge.badgeValue = giftCount - } else { - giftCountBadge.isHidden = true - } - - // 更新描述文本,包含礼物信息 - if let giftName = gift.name, giftCount > 0 { - let giftText = "Thank you for your \(giftCount) \(giftName)" - descLabel.text = giftText - }else if let giftName = gift.name{ - let giftText = "Thank you for your \(giftName)" - descLabel.text = giftText - } - } - - /// 公共方法:配置页面数据 - func configData(card: MeetCard?, gift: GiftDictModel?, giftCount: Int = 0) { - self.card = card - self.gift = gift - self.giftCount = giftCount - - // 重新配置数据 - configCardData() - configGiftData() - } - - // MARK: - Action - - @objc func chatButtonAction() { - guard let aiId = card?.aiId else{ - dismiss(animated: false) - return - } - self.dismiss(animated: true) { - AppRouter.goChatVC(aiId: aiId) - } - } - - @objc func cancelButtonAction() { -// self.removeFromParent() -// self.view.removeFromSuperview() - close() - } -} diff --git a/crush/Crush/Src/Modules/Home/View/EncounterAICardAlbumView.swift b/crush/Crush/Src/Modules/Home/View/EncounterAICardAlbumView.swift deleted file mode 100644 index 48569d8..0000000 --- a/crush/Crush/Src/Modules/Home/View/EncounterAICardAlbumView.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// EncounterAICardAlbumView.swift -// Crush -// -// Created by Leon on 2025/9/11. -// -import UIKit - -class EncounterAICardAlbumView: UIView { - var cv: UICollectionView! - var layout: UICollectionViewFlowLayout! - - // 数据源 - var card: MeetCard? - private var albumItems: [AlbumPhotoItem] = [] - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - //backgroundColor = .random - - let cardWidth = UIScreen.width - 12 * 2 - let itemW = floor((cardWidth - 24 * 2 - 16) * 0.5) as CGFloat - let itemH = itemW - - layout = UICollectionViewFlowLayout() - layout.itemSize = CGSize(width: itemW, height: itemH) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 12, left: 24, bottom: 12 + UIWindow.safeAreaBottom, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.showsVerticalScrollIndicator = false - cv.register(RoleHomeAlbumGridCell.self, forCellWithReuseIdentifier: "RoleHomeAlbumGridCell") - cv.delegate = self - cv.dataSource = self - cv.isScrollEnabled = false - addSubview(cv) - - cv.snp.makeConstraints { make in - make.edges.equalToSuperview() - make.height.equalTo(764) // Temp height! - } - } - - // MARK: - Public Methods - func configCard(card: MeetCard?){ - self.card = card - } - - func config(_ albumItems: [AlbumPhotoItem]) { - - self.albumItems = albumItems - cv.reloadData() - - // 计算动态高度 - let cardWidth = UIScreen.width - 12 * 2 - let itemW = floor((cardWidth - 24 * 2 - 16) * 0.5) as CGFloat - let itemH = itemW - - let itemsPerRow = 2 // 每行2个item - let lineSpacing: CGFloat = 16 - let topInset: CGFloat = 12 - let bottomInset: CGFloat = 12 + UIWindow.safeAreaBottom - - var calculatedHeight: CGFloat - - if albumItems.count > 0 { - // 有图片时,根据实际数量计算行数 - let rowCount = ceil(CGFloat(albumItems.count) / CGFloat(itemsPerRow)) - calculatedHeight = topInset + (rowCount * itemH) + ((rowCount - 1) * lineSpacing) + bottomInset - } else { - // 无图片时,留足两行cell高度给占位图 - let emptyRowCount: CGFloat = 2 - calculatedHeight = topInset + (emptyRowCount * itemH) + ((emptyRowCount - 1) * lineSpacing) + bottomInset - } - - // 更新collectionView的高度约束 - cv.snp.updateConstraints { make in - make.height.equalTo(calculatedHeight) - } - - if albumItems.count > 0{ - hideEmpty() - }else{ - showEmpty(text: "No photos") - } - } -} - -extension EncounterAICardAlbumView: UICollectionViewDelegate, UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return albumItems.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoleHomeAlbumGridCell", for: indexPath) as! RoleHomeAlbumGridCell - cell.usedInMeetCard = true - let albumItem = albumItems[indexPath.item] - cell.config(data: albumItem) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - print("did Select Meet album ItemAt \(indexPath)") - guard let aiId = card?.aiId else {return} - - var photoModels = [PhotoBrowserModel]() - - for (index, per) in albumItems.enumerated() { - let perAlbum = per - - let model = PhotoBrowserModel() - model.aiAlbum = perAlbum - model.aiId = aiId - model.imageUrl = perAlbum.getImgUrl() - if index == indexPath.item{ - if let cell = collectionView.cellForItem(at: indexPath) as? RoleHomeAlbumGridCell{ - model.image = cell.imageView.image - model.sourceRect = cell.screenRect ?? .zero - } - } - photoModels.append(model) - } - - ImageBrowser.show(models: photoModels, index: indexPath.item, type: .roleOthersInAlbum) - } -} diff --git a/crush/Crush/Src/Modules/Home/View/EncounterAICardHeader.swift b/crush/Crush/Src/Modules/Home/View/EncounterAICardHeader.swift deleted file mode 100644 index 73e121f..0000000 --- a/crush/Crush/Src/Modules/Home/View/EncounterAICardHeader.swift +++ /dev/null @@ -1,451 +0,0 @@ -// -// EncounterAICardHeader.swift -// Crush -// -// Created by Leon on 2025/9/11. -// - -class EncounterAICardHeader:UIView{ - var imageFieldContainer: UIView! - var imageBgButton: UIButton! - var imageView: UIImageView! - var topRightTag: EPTagLabel! - - var overlay: GradientView! - var overlayBgButton: UIButton! - var bottomButtonsStackH : UIStackView! - var dislikeButton: UIButton! - var giftButton : EPIconPrimaryButton! - var likeButton : UIButton! - - var likeAndCount: CLIconLabel! - var nameLabel: CLLabel! - var tagsStackH: UIStackView! - var introductionStackH: UIStackView! - var introductionLabel: LineSpaceLabel! - var introductionLabelTopButton: UIButton! - var expendButton: EPIconGhostButton! - var aiGeneratedLabel: CLLabel! - - var albumTitleView: UIView! - var albumTitleLabel : CLLabel! - - var data: MeetCard? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - imageFieldContainer = { - let v = UIView() - v.backgroundColor = .clear - v.clipsToBounds = true - addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(EncounterAICardView.cardHeight) - } - return v - }() - - imageBgButton = { - let v = UIButton() - imageFieldContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - imageView = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - imageFieldContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - topRightTag = { - let v = EPTagLabel(style: .primary, size: .large) - imageFieldContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - overlay = { - let color = UIColor.c.cbd - let v = GradientView(colors: [color.withAlphaComponent(0), color.withAlphaComponent(0.6), color.withAlphaComponent(0.9)], gradientType: .topToBottom) - imageFieldContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - make.height.equalTo(368) - } - return v - }() - - bottomButtonsStackH = { - let v = UIStackView() - v.spacing = 32 - v.alignment = .center - imageFieldContainer.addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview().offset(-40) - make.centerX.equalToSuperview() - } - return v - }() - - dislikeButton = { - let v = UIButton() - let image = UIImage(named: "encounter_icon_dislike") - v.setImage(image, for: .normal) - v.backgroundColor = .c.cseln - v.cornerRadius = 32 - v.contentMode = .center - bottomButtonsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 64, height: 64)) - } - return v - }() - - giftButton = { - let v = EPIconPrimaryButton(radius: .round, iconSize: .large, iconCode: .giftBorder) - bottomButtonsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 48, height: 48)) - } - return v - }() - - likeButton = { - let v = UIButton() - let image = UIImage(named: "encounter_icon_like") - v.setImage(image, for: .normal) - v.backgroundColor = .c.cseln - v.cornerRadius = 32 - v.contentMode = .center - bottomButtonsStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 64, height: 64)) - } - return v - }() - - setupMiddlePart() - setupAlbumTitlePart() - - overlayBgButton = { - let v = UIButton() - overlay.insertSubview(v, at: 0) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.bottom.equalTo(introductionStackH.snp.top).offset(-8) - } - return v - }() - -// #warning("test") -// testData() - } - - private func setupMiddlePart(){ - nameLabel = { - let v = CLLabel() - v.numberOfLines = 1 // 暂时限制1行 - v.font = .t.thl - overlay.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-96) - make.bottom.equalTo(overlay.snp.top).offset(112) - } - return v - }() - - likeAndCount = { - let v = CLIconLabel() - v.iconSize = CGSize(width: 20, height: 20) - v.contentLabel.font = .t.tnds - v.iconImageView.image = MWIconFont.image(fromIcon: .like, size: CGSize(width: 20, height: 20), color: .white) - overlay.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-24) - make.centerY.equalTo(nameLabel) - } - return v - }() - - tagsStackH = { - let v = UIStackView() - v.spacing = 8 - overlay.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalTo(nameLabel.snp.bottom).offset(8) - } - return v - }() - - introductionStackH = { - let v = UIStackView() - v.spacing = 0 - v.alignment = .bottom - overlay.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(tagsStackH.snp.bottom).offset(8) - } - return v - }() - - introductionLabel = { - let v = LineSpaceLabel() - let typography = CLSystemToken.typography(token: .tbm) - v.textColor = .text - v.config(typography) - v.numberOfLines = 3 - introductionStackH.addArrangedSubview(v) - return v - }() - - introductionLabelTopButton = { - let v = UIButton() - v.addTarget(self, action: #selector(tapIntroductionTop), for: .touchUpInside) - overlay.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalTo(introductionLabel) - } - return v - }() - - expendButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .xs, iconCode: .iconFullimage) - v.addTarget(self, action: #selector(tapExpandButton), for: .touchUpInside) - introductionStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - v.isHidden = true // 默认隐藏 - return v - }() - - aiGeneratedLabel = { - let v = CLLabel() - v.textColor = .c.ctsn - v.font = .t.tbs - overlay.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(introductionStackH.snp.bottom).offset(8) - } - return v - }() - } - - private func setupAlbumTitlePart(){ - albumTitleView = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalTo(imageFieldContainer.snp.bottom).offset(16) - make.bottom.equalToSuperview().offset(-24) - make.height.equalTo(32) - } - return v - }() - - albumTitleLabel = { - let v = CLLabel() - v.font = .t.ttm - albumTitleView.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - do{ - let left = { - let v = CLLine() - albumTitleView.addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(1) - make.leading.equalToSuperview() - make.trailing.equalTo(albumTitleLabel.snp.leading).offset(-16) - make.centerY.equalTo(albumTitleLabel) - } - return v - }() - - let right = { - let v = CLLine() - albumTitleView.addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(1) - make.trailing.equalToSuperview() - make.leading.equalTo(albumTitleLabel.snp.trailing).offset(16) - make.centerY.equalTo(albumTitleLabel) - } - return v - }() - - left.isHidden = false - right.isHidden = false - } - - albumTitleLabel.text = "Album" - topRightTag.text = "Secret Admirer" - topRightTag.isHidden = true - } - - private func testData(){ - - nameLabel.text = "Alice, 24" - likeAndCount.contentLabel.text = "1.2k" - - do { - let tag = RoleTag() - tag.title = "Sensibility" - tag.style = .blurPurple - tagsStackH.addArrangedSubview(tag) - } - - do { - let tag = RoleTag() - tag.title = "Romantic" - tag.style = .blurTheme - tagsStackH.addArrangedSubview(tag) - } - - introductionLabel.text = "She is a new trainee teacher who has just graduated. You are the most rebellious student in the whole school. In order to prove her ability, she took the initiative to apply to be your class teacher. In the upcoming college entrance examination, you two…" - aiGeneratedLabel.text = "Content generated by AI" - - imageView.image = UIImage.egpic - } - - // MARK: - Public Methods - - func config(_ card: MeetCard) { - data = card - - // 配置头像 - if let imageUrl = card.imageUrl { - imageView.loadImage(imageUrl) - } - - // 配置右上角标签 - if data?.secretAdmirer ?? false{ - topRightTag.isHidden = false - }else { - topRightTag.isHidden = true - } - - // 配置姓名和年龄 - let name = card.nickname ?? "Unknown" - let age = card.age != nil ? ", \(card.age!)" : "" - nameLabel.text = "\(name)\(age)" - - // 配置点赞数 - let likeCount = card.likedCount ?? 0 - likeAndCount.contentLabel.text = formatLikeCount(likeCount) - - // 清除现有标签 - tagsStackH.arrangedSubviews.forEach { $0.removeFromSuperview() } - - // 配置性格标签 - if let character = card.character, !character.isEmpty { - let tag = RoleTag() - tag.title = character - //tag.style = .blurTheme - tag.style = .default - tagsStackH.addArrangedSubview(tag) - } - - // 配置标签 - if let tag = card.tag, !tag.isEmpty { - let tagView = RoleTag() - tagView.title = tag - //tagView.style = .blurPurple - tagView.style = .default - tagsStackH.addArrangedSubview(tagView) - } - - // 配置简介 - introductionLabel.text = card.introduction ?? "" - updateExpandButtonVisibility() // 添加这行来更新按钮显示状态 - aiGeneratedLabel.text = "Content generated by AI" - } - - private func formatLikeCount(_ count: Int) -> String { - if count >= 1000 { - return String(format: "%.1fk", Double(count) / 1000.0) - } else { - return "\(count)" - } - } - - private func updateExpandButtonVisibility() { - guard let text = introductionLabel.text, !text.isEmpty else { - expendButton.isHidden = true - return - } - - // 计算文本在3行限制下的实际高度 - let maxWidth = introductionLabel.frame.width > 0 ? introductionLabel.frame.width : (UIScreen.main.bounds.width - 48) // 减去左右边距 - let maxSize = CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude) - - // 计算3行文本的高度 - let threeLineHeight = introductionLabel.font.lineHeight * 3 + introductionLabel.font.lineHeight * 2 // 3行 + 2个行间距 - - // 计算实际文本需要的高度 - let actualSize = text.boundingRect( - with: maxSize, - options: [.usesLineFragmentOrigin, .usesFontLeading], - attributes: [.font: introductionLabel.font!], - context: nil - ) - - // 如果实际高度超过3行高度,显示展开按钮 - let hideBrowseIntroduction = actualSize.height <= threeLineHeight - expendButton.isHidden = hideBrowseIntroduction - introductionLabelTopButton.isHidden = hideBrowseIntroduction - } - - // 重写layoutSubviews来在布局完成后更新按钮状态 - override func layoutSubviews() { - super.layoutSubviews() - updateExpandButtonVisibility() - } - - // MARK: - Action - - @objc private func tapExpandButton(){ - guard let introduction = data?.introduction else{return} - - let vc = RoleIntroBrowseDialogController() - vc.introduction = introduction - vc.imageUrl = data?.imageUrl - vc.show() - } - - @objc private func tapIntroductionTop(){ - tapExpandButton() - } -} diff --git a/crush/Crush/Src/Modules/Home/View/EncounterAICardView.swift b/crush/Crush/Src/Modules/Home/View/EncounterAICardView.swift deleted file mode 100644 index fa77164..0000000 --- a/crush/Crush/Src/Modules/Home/View/EncounterAICardView.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// EncounterAICardView.swift -// Crush -// -// Created by Leon on 2025/9/10. -// - -import UIKit -import Combine - -protocol EncounterAICardViewDelegate: AnyObject{ - func cardViewTapLike(_ obj: MeetCard?) - func cardViewTapGift(_ obj: MeetCard?) - func cardViewTapDislike(_ obj: MeetCard?) - func cardViewTapHeader(_ obj: MeetCard?, cardView: EncounterAICardView?) -} - -class EncounterAICardView: MeetDragCardView { - - static let cardWidth = UIScreen.width - 12 * 2.0 - static let cardHeight = UIScreen.height - UIWindow.tabBarTotalHeight - UIWindow.navBarTotalHeight - 12 - 12 - - var scrollContainer: LTScrollContainer! - var indicator: PageIndicatorView! - - var header: EncounterAICardHeader! - var albumView: EncounterAICardAlbumView! - - weak var delegate : EncounterAICardViewDelegate? - - var card: MeetCard? - - // Flag - var isPullingToAIHomePage = false - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.cbd - - scrollContainer = { - let v = LTScrollContainer() - v.scrollView.delegate = self - v.scrollView.bounces = true - v.scrollView.showsVerticalScrollIndicator = false - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - indicator = { - let v = PageIndicatorView() - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-4) - } - return v - }() - - header = { - let v = EncounterAICardHeader() - scrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - albumView = { - let v = EncounterAICardAlbumView() - scrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - } - - private func setupData(){ - - } - - private func setupEvent(){ - header.dislikeButton.addTarget(self, action: #selector(tapDislikeButton), for: .touchUpInside) - header.likeButton.addTarget(self, action: #selector(tapLikeButton), for: .touchUpInside) - header.giftButton.addTarget(self, action: #selector(tapGiftButton), for: .touchUpInside) - header.imageBgButton.addTarget(self, action: #selector(tapImageBgButton), for: .touchUpInside) - header.overlayBgButton.addTarget(self, action: #selector(tapImageBgButton), for: .touchUpInside) - - PhotosViewModel.shared.$album.sink {[weak self] photo in - guard let self = self else{return} - guard let album = photo , let albumId = photo?.albumId else {return} - - let albumList = self.card?.albumList ?? [] - for per in albumList{ - let perAlbum = per - if albumId == perAlbum.albumId{ - perAlbum.updateFrom(album) - self.albumView.config(albumList) - break - } - } - }.store(in: &cancellables) - } - - // MARK: - Public - - func config(_ data: MeetCard?){ - self.card = data - guard let card = data else { return } - - // 配置Header数据 - header.config(card) - - // 配置相册数据 - albumView.configCard(card: card) - albumView.config(card.albumList ?? []) - } - - // MARK: - Action - - @objc func tapDislikeButton(){ - delegate?.cardViewTapDislike(card) - } - - @objc func tapLikeButton(){ - delegate?.cardViewTapLike(card) - } - - @objc func tapGiftButton(){ - delegate?.cardViewTapGift(card) - } - - @objc func tapImageBgButton(){ - delegate?.cardViewTapHeader(card, cardView: self) - } -} - -extension EncounterAICardView: UIScrollViewDelegate{ - func scrollViewDidScroll(_ scrollView: UIScrollView) { - // 计算滚动进度 (0.0 到 1.0) - let maxOffset = scrollView.contentSize.height - scrollView.bounds.size.height - let progress = maxOffset > 0 ? scrollView.contentOffset.y / maxOffset : 0 - //dlog("progress:\(progress)") - // 更新指示器 - //indicator.updateIndicator(progress: progress) - indicator.updateIndicator(top: progress >= 0.5) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let offsetY = scrollView.contentOffset.y - let contentHeight = scrollView.contentSize.height - let height = scrollView.frame.size.height - - if offsetY > contentHeight - height + 40 { - - guard let aiId = card?.aiId else{return} - //print("松手时,滑到底部之外了") - if isPullingToAIHomePage == false{ - CLTool.feedbackGenerator() - AppRouter.goAIRoleHome(aiId: aiId) - - isPullingToAIHomePage = true - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {[weak self] in - self?.isPullingToAIHomePage = false - } - } else if offsetY < 0 { - //print("松手时,滑到顶部之外了") - } - } -} diff --git a/crush/Crush/Src/Modules/Home/View/HomePageRootView.swift b/crush/Crush/Src/Modules/Home/View/HomePageRootView.swift deleted file mode 100644 index 20186f4..0000000 --- a/crush/Crush/Src/Modules/Home/View/HomePageRootView.swift +++ /dev/null @@ -1,574 +0,0 @@ -// -// HomePageRootView.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -class HomePageRootView: UIView { - var emptyView: UIView! - - private var cardContainer: UIView! - private var bgTopIv: UIImageView! - private var dragCardContainer: MeetDragCardContainer! - private var dataArray: [MeetCard] = [] - - - // Flag - var swipeCardCount: Int = 0 // 计数 - - weak var viewModel: MeetViewModel! - weak var weakGiftSheet: GiftAIRoleSheet? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - - emptyView = { - let v = UIView() - v.showEmpty(text: "暂无卡片") - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview().offset(-UIWindow.tabBarTotalHeight) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - } - return v - }() - - cardContainer = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - // 创建拖拽卡片容器 - bgTopIv = { - let v = UIImageView() - v.image = UIImage(named: "meet_top_bg") - cardContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(200/393.0) - } - return v - }() - - let cardWidth = EncounterAICardView.cardWidth - let cardHeight = EncounterAICardView.cardHeight - dragCardContainer = MeetDragCardContainer(frame: CGRect(x: 0, y: 0, width: cardWidth, height: cardHeight)) - cardContainer.addSubview(dragCardContainer) - - dragCardContainer.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight + 12) - make.width.equalTo(cardWidth) - make.height.equalTo(cardHeight) - } - - // 设置数据源和代理 - dragCardContainer.dataSource = self - dragCardContainer.delegate = self - - // 初始状态都隐藏 - emptyView.isHidden = true - //cardContainer.isHidden = true - } - - private func setupData(){ - setupDragCardContainer() - } - - private func setupEvent(){ - - } - - private func setupDragCardContainer() { - // 重新加载数据 - dragCardContainer.reloadData() - } - - // MARK: - mark - - func setupViewModel(vm: MeetViewModel){ - viewModel = vm - // 绑定ViewModel回调 - viewModel?.onCardsLoaded = { [weak self] cards in - self?.dataArray = cards - self?.dragCardContainer.reloadData() - - if self?.viewModel.requestParams.pn == 1{ - self?.showEmptyPlaceholder(empty: cards.count == 0) - } - } - - viewModel?.onCardsMoreLoaded = { [weak self] cards in - if cards.count == 0 { - self?.showEmptyPlaceholder(empty: true) - } - } - - viewModel?.onLoadError = { error in - dlog("Meet加载卡片失败: \(error)") - } - } - - // MARK: - Functions - - private func doReportLike(card: MeetCard?){ - guard let aiId = card?.aiId else {return} - - swipeCardCount += 1 - - var params = [String:Any]() - params.updateValue(aiId, forKey: "aiId") - params.updateValue(true, forKey: "lk") - - DiscoverProvider.request(.meetLikeOrReport(params: params), modelType: MeetCardReportResponse.self) {[weak self] result in - switch result { - case .success(let model): - self?.dealLikeAfter(aiId: aiId,card: card, obj: model) - case .failure: - break - } - } - } - - private func doReportDislike(card: MeetCard?){ - guard let aiId = card?.aiId else {return} - - swipeCardCount += 1 - - var params = [String:Any]() - params.updateValue(aiId, forKey: "aiId") - params.updateValue(true, forKey: "lk") - - DiscoverProvider.request(.meetLikeOrReport(params: params), modelType: MeetCardReportResponse.self) {result in - switch result { - case .success: - break // do nothing - case .failure: - break - } - } - } - - private func dealLikeAfter(aiId: Int, card: MeetCard?, obj:MeetCardReportResponse?){ - guard let response = obj else {return} - // ... - if response.bd.boolValue { -// #warning("test") -// let vc = MeetMatchedController() -// vc.card = card! -//// vc.modalPresentationStyle = .fullScreen -// let navc = CLNavigationController(rootViewController: vc) -//// self?.viewController()?.present(navc, animated: true) -// UIWindow.getTopViewController()?.present(navc, animated: true) -// return - // 调用绑定 - DiscoverProvider.request(.meetReportBind(aiId: aiId), modelType: MeetMatchInfo.self) { result in - switch result { - case .success(let model): - if let resultObj = model{ - let vc = MeetMatchedController() - vc.info = resultObj - let navc = CLNavigationController(rootViewController: vc) - UIWindow.getTopViewController()?.present(navc, animated: true) - }else{ - dlog("❌meetReportBind result is nil") - let vc = MeetMatchedController() - vc.card = card! - let navc = CLNavigationController(rootViewController: vc) -// self?.viewController()?.present(navc, animated: true) - UIWindow.getTopViewController()?.present(navc, animated: true) - } - case .failure: - break - } - } - } - - if response.rc.boolValue{ - DiscoverProvider.request(.meetRcAndRetrunBlurAlbum, modelType: MeetReportAdmirerReturnAlbum.self) {[weak self] result in - switch result { - case .success(let model): - self?.doShowSecretAdmirerAlert(aiId: aiId, photo: model) - case .failure: - break - } - } - } - } - - private func doShowSecretAdmirerAlert(aiId: Int?, photo: MeetReportAdmirerReturnAlbum?){ - guard let thePhoto = photo else {return} - - let content = "Someone is secretly in love with you\n" - let alert = Alert(title: "Secret Admirer", text: content, image: UIImage(named: "heart_meet_48")!) - - alert.masTopToImageBgForImageView?.update(offset: 42) - alert.setupContentStackSpacing(14) - alert.setupBottomBlurImageUrl(url: thePhoto.img1) - - //let coin = Coin(cents: thePhoto.unlockPrice) - let coin = Coin(cents: 5000) // 50 coin - let string = "\(coin.formatted) unlock" - let aString = NSAttributedString.getIconTitleAttributeByWords(words: string, iconImage: UIImage.icon32Diamond, iconSize: CGSize(width: 24, height: 24), textFont: .t.tll, textColor: .white) - let confirmCoin = AlertAction(attributedTitle: aString, actionStyle: .confirm) {[weak self] in - // 解锁,🔓 - self?.doUnlockLikeYou(aiId: aiId, albumId: photo?.albumId) - } - - let cancel = AlertAction(title: "Cancel", actionStyle: .cancel, block: nil) - - alert.addAction(confirmCoin) - alert.addAction(cancel) - alert.show() - alert.layoutIfNeeded() - } - - private func doUnlockLikeYou(aiId: Int? , albumId: Int?){ - guard let theAIid = aiId, let theAlbumId = albumId else { - return - } - - Hud.showIndicator() - DiscoverProvider.request(.meetUnlockLikeYou(aiId: theAIid, albumId: theAlbumId), modelType: AIAlbumUnlockImages.self) {[weak self] result in - switch result { - case .success: - self?.doGetLikeYouAICardInfo(aiId: theAIid) - case .failure: - Hud.hideIndicator() - } - } - - } - - func doGetLikeYouAICardInfo(aiId: Int?){ - guard let theAiId = aiId else{ - Hud.hideIndicator() - return - } - - DiscoverProvider.request(.meetCardDetail(aiId: theAiId), modelType: MeetCard.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - model?.secretAdmirer = true - self?.doRestoreCard(card: model) - case .failure: - break - } - } - } - - private func doRestoreCard(card : MeetCard?){ - guard let data = card else {return} - if let tmpStoreNopeCardView = dragCardContainer.tmpStoreNopeCardView as? EncounterAICardView{ - tmpStoreNopeCardView.config(data) - dragCardContainer.addCardView(tmpStoreNopeCardView, fromDirection: .default) - }else{ - dlog("❌ none tmpStoreNopeCardView") - } - } - - // MARK: - Helper - - func showEmptyPlaceholder(empty: Bool){ - emptyView.isHidden = !empty - cardContainer.isHidden = empty - } - - // MARK: - Test - - func testFunction(){ - doGetLikeYouAICardInfo(aiId: 444190968774657) - } - -} - -// MARK: - EncounterAICardViewDelegate -extension HomePageRootView: EncounterAICardViewDelegate { - func cardViewTapLike(_ obj: MeetCard?) { - print("点击了喜欢按钮: \(String(describing: obj?.nickname))") - if UserCore.shared.isLogin() == false && swipeCardCount >= 3{ - UserCore.shared.checkUserLoginIfNotPushUserToLogin() - return - } - - dragCardContainer.removeCardViewForDirection(.right) - doReportLike(card: obj) - } - - func cardViewTapGift(_ obj: MeetCard?) { - // print("点击了礼物按钮: \(String(describing: obj?.nickname))") - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{ - return - } - - let vc = GiftAIRoleSheet() - vc.giftView.delegate = self - vc.giftView.targetHeartbeatValue = obj?.heartbeatVal ?? 0 - weakGiftSheet = vc - vc.show() - } - - func cardViewTapDislike(_ obj: MeetCard?) { - print("点击了不喜欢按钮: \(String(describing: obj?.nickname))") - if UserCore.shared.isLogin() == false && swipeCardCount >= 3{ - UserCore.shared.checkUserLoginIfNotPushUserToLogin() - return - } - - dragCardContainer.removeCardViewForDirection(.left) - doReportDislike(card: obj) - } - - func cardViewTapHeader(_ obj: MeetCard?, cardView: EncounterAICardView?){ - guard let data = obj, let albumList = data.albumList, let aiId = obj?.aiId else{return} - - let screenRect = cardView?.screenRect ?? .zero - let currentImageUrl = obj?.imageUrl ?? "" - - var photoModels = [PhotoBrowserModel]() - - var browseIndex = 0 - for (index, per) in albumList.enumerated() { - let model = PhotoBrowserModel() - model.aiAlbum = per - - model.aiId = aiId - model.imageUrl = per.getImgUrl() - model.image = cardView?.header.imageView.image ?? UIImage() - model.sourceRect = screenRect - - if model.imageUrl == currentImageUrl{ - browseIndex = index - } - - photoModels.append(model) - } - - ImageBrowser.show(models: photoModels, index: browseIndex, type: .roleOthersInAlbum) - } -} - -extension HomePageRootView: GiftGridSendViewDelegate{ func giftSend(giftId: Int, gift: GiftDictModel, num: Int){ - guard let card = dragCardContainer.getCurrentShowCardView() as? EncounterAICardView, let info = card.card else{ - return - } - guard let aiId = info.aiId else{ - return - } - - #warning("test") - weakGiftSheet?.dismiss() - dragCardContainer.removeCardViewForDirection(.right) - let vc = MeetMatchedController() - vc.card = info - vc.gift = gift - vc.giftCount = num - let navc = CLNavigationController(rootViewController: vc) - UIWindow.getTopViewController()?.present(navc, animated: true) - return - -// var params = [String: Any]() -// -// params.updateValue(aiId, forKey: "aiId") -// params.updateValue(giftId, forKey: "giftId") -// params.updateValue(num, forKey: "num") -// params.updateValue("HOME", forKey: "scene") -// -// Hud.showIndicator() -// ChatProvider.request(.aiUserGiftSend(params: params), modelType: EmptyModel.self) {[weak self] result in -// Hud.hideIndicator() -// switch result { -// case .success: -// self?.weakGiftSheet?.dismiss() -// -// self?.dragCardContainer.removeCardViewForDirection(.right) -// let vc = MeetMatchedController() -// vc.card = info -// vc.gift = gift -// vc.giftCount = num -// let navc = CLNavigationController(rootViewController: vc) -// UIWindow.getTopViewController()?.present(navc, animated: true) -// case .failure: -// break -// } -// } - } - - func giftGoHeartbeatPage(){ - guard let card = dragCardContainer.getCurrentShowCardView() as? EncounterAICardView, let info = card.card else{ - return - } - guard let aiId = info.aiId else{ - return - } - weakGiftSheet?.dismiss() - AppRouter.goAIHeartBeatLevelPage(aiId: aiId) - } - - func giftGoVIPCenter(){ - weakGiftSheet?.dismiss() - AppRouter.goVIPCenter() - } - - func giftNumPick(num: Int){ - guard let window = UIWindow.getTopDisplayWindow(), let sheet = weakGiftSheet else { return } // 🔥 Assuming topDisplayWindow is a custom UIView extension method - - guard let giftSendView = sheet.giftView else{ - return - } - - let rect1 = giftSendView.countImg.convert(giftSendView.countImg.frame, from: giftSendView.countImg.superview) - let rect2 = giftSendView.countImg.convert(rect1, to: window) - - let vc = GiftCountChooseController(sourceRect: rect2, selectedValue: "\(num)") - vc.chooseBlock = { [weak self] count in - // ... Handle count selection - self?.weakGiftSheet?.giftView.num = Int(count) ?? 1 - } - - vc.popoverDismiss = { [weak self] in - // ... Handle popover dismissal - self?.weakGiftSheet?.giftView.countImg.transform = .identity - } - - viewController()?.navigationController?.addChild(vc) - sheet.addSubview(vc.view) - - UIView.animate(withDuration: 0.2) { [weak self] in - self?.weakGiftSheet?.giftView.countImg.transform = CGAffineTransform(rotationAngle: .pi) - } - } -} - -extension HomePageRootView :MeetDragCardContainerDataSource { - func numberOfRowsInYFLDragCardContainer(_ container: MeetDragCardContainer) -> Int { - return dataArray.count - } - - func container(_ container: MeetDragCardContainer, viewForRowsAt index: Int) -> MeetDragCardView { - dlog("🐟index:\(index)") - let cardView = EncounterAICardView() - let card = dataArray[index] - cardView.config(card) - cardView.delegate = self - return cardView - } -} - -// MARK: - MeetDragCardContainerDelegate - -extension HomePageRootView: MeetDragCardContainerDelegate { - func container(_ container: MeetDragCardContainer, didSelectRowAt index: Int) { - print("「Meet」点击了整体卡片: \(index)") - } - - func container(_ container: MeetDragCardContainer, dataSourceIsEmpty isEmpty: Bool) { - if isEmpty { - print("「Meet」数据源为空,可以加载更多数据") - // 这里可以加载更多数据 - - if viewModel.cards.count > 0{ // 说明是后续加载 - loadMoreData() - // showEmptyPlaceholder(empty: true) - } - } - } - - func container(_ container: MeetDragCardContainer, canDragForCardView cardView: MeetDragCardView) -> Bool { - return true - } - - func container(_ container: MeetDragCardContainer, dargingForCardView cardView: MeetDragCardView, direction: ContainerDragDirection, widthRate: CGFloat, heightRate: CGFloat) { - // 拖拽过程中的回调 - if direction == .left { - container.showNopeLogo(true, widthRate: CGFloat(abs(widthRate))) - print("🐟左滑ing \(widthRate)") - } else if direction == .right { - container.showLikeLogo(true, widthRate: CGFloat(abs(widthRate))) - print("🐟右滑ing \(widthRate)") - } else { - container.showNopeLogo(false, widthRate: 0) - container.showLikeLogo(false, widthRate: 0) - } - } - - func container(_ container: MeetDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) -> Bool { - if UserCore.shared.isLogin(){ - return true - } - - if swipeCardCount >= 3{ - return UserCore.shared.checkUserLoginIfNotPushUserToLogin() - } - - return true - } - - func container(_ container: MeetDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) { - print("🐟卡片拖拽完成,方向: \(direction)") - let aiCardView = cardView as! EncounterAICardView - // 显示相应的动画 - switch direction { - case .left: - //container.showNopeLottie() - container.showNopeLogo() - doReportDislike(card: aiCardView.card) - case .right: - //container.showLikeLottie() - container.showLikeLogo() - doReportLike(card: aiCardView.card) - default: - break - } - - // 检查是否需要预加载 - checkAndPreloadIfNeeded() - } - - func container(_ container: MeetDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) { - print("🐟卡片回看,方向: \(direction)") - } - - func container(_ container: MeetDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: MeetDragCardView) { - print("🐟进入小卡片模式: \(smallCardMode)") - } - - private func loadMoreData() { - dlog("🐟try load more datas") - // 加载更多数据 - viewModel?.loadMoreCards() - } - - // 检查并预加载数据 - private func checkAndPreloadIfNeeded() { - guard let viewModel = viewModel else { return } - - // 计算剩余卡片数量 - let remainingCount = dataArray.count - dragCardContainer.getCurrentShowCardViewIndex() - - // 如果剩余卡片数量少于等于5张,且不在加载中,则预加载 - if viewModel.shouldPreload(remainingCount: remainingCount) { - dlog("🐟预加载数据,剩余卡片数量: \(remainingCount)") - loadMoreData() - } - } -} diff --git a/crush/Crush/Src/Modules/Home/ViewModel/MeetViewModel.swift b/crush/Crush/Src/Modules/Home/ViewModel/MeetViewModel.swift deleted file mode 100644 index 095902d..0000000 --- a/crush/Crush/Src/Modules/Home/ViewModel/MeetViewModel.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// MeetViewModel.swift -// Crush -// -// Created by Leon on 2025/9/12. -// - -import UIKit - -class MeetViewModel{ - - lazy var requestParams = AIRoleListRequest() - - /// 可选的筛选参数 - //var filterParams = [String:Any]() - var filterModel : RolesFilterModel? - - // 数据存储 - var cards: [MeetCard] = [] - - // 回调闭包 - /// 回调所有数据 - var onCardsLoaded: (([MeetCard]) -> Void)? - /// 仅分页loadMore - var onCardsMoreLoaded: (([MeetCard]) -> Void)? - var onLoadError: ((Error) -> Void)? - - // 预加载相关 - private var isLoadingMore = false - - func loadCards(completion: ((Bool, [MeetCard]) -> Void)? = nil){ - - var params = requestParams.toNonNilDictionary() - - //dicts = dicts.merged(with: filterParams) - if let filterCondition = filterModel{ - // 筛选条件 - if let sexs = filterCondition.sex, sexs.count > 0, let firstSex = sexs.first{ // 后续可能会改成多选 - params.updateValue(firstSex.rawValue, forKey: "sex") - } - - if let age = filterCondition.age{ - params.updateValue(age.rawValue, forKey: "age") - } - - if let roleCodes = filterCondition.roleCodeList, roleCodes.count > 0{ - params.updateValue(roleCodes, forKey: "roleCodeList") - } - } - - DiscoverProvider.request(.homeRecommentCardList(params: params), modelType: [MeetCard]?.self) {[weak self] result in - guard let self = self else{return} - switch result { - case .success(let model): - if let cardsDatas = model { - self.cards = cardsDatas ?? [] - DispatchQueue.main.async { - self.onCardsLoaded?(cardsDatas ?? []) - } - completion?(true, cardsDatas ?? []) - } - case .failure(let error): - DispatchQueue.main.async { - self.onLoadError?(error) - completion?(false, []) - } - } - } - } - - func loadMoreCards() { - guard !isLoadingMore else { return } - isLoadingMore = true - requestParams.pn += 1 - - var params = requestParams.toNonNilDictionary() - if let filterCondition = filterModel{ - // 筛选条件 - if let sexs = filterCondition.sex, sexs.count > 0, let firstSex = sexs.first{ // 后续可能会改成多选 - params.updateValue(firstSex.rawValue, forKey: "sex") - } - - if let age = filterCondition.age{ - params.updateValue(age.rawValue, forKey: "age") - } - - if let roleCodes = filterCondition.roleCodeList, roleCodes.count > 0{ - params.updateValue(roleCodes, forKey: "roleCodeList") - } - } - - Hud.showIndicator() - DiscoverProvider.request(.homeRecommentCardList(params: params), modelType: [MeetCard]?.self) {[weak self] result in - Hud.hideIndicator() - - guard let self = self else{return} - self.isLoadingMore = false - - switch result { - case .success(let model): - // dlog(model) - if let cardsDatas = model { -// #warning("test") -// self.cards.append(contentsOf: []) -// DispatchQueue.main.async { -// self.onCardsLoaded?(self.cards) -// self.onCardsMoreLoaded?([]) -// } -// return - // 追加新数据而不是替换 - - self.cards.append(contentsOf: cardsDatas ?? []) - DispatchQueue.main.async { - self.onCardsLoaded?(self.cards) - self.onCardsMoreLoaded?(cardsDatas ?? []) - } - } - case .failure(let error): - // 加载失败时回退页码 - self.requestParams.pn -= 1 - DispatchQueue.main.async { - self.onLoadError?(error) - } - } - } - } - - func resetAndLoadCards() { - requestParams.pn = 1 - cards.removeAll() - isLoadingMore = false - loadCards() - } - - // 检查是否需要预加载 - func shouldPreload(remainingCount: Int) -> Bool { - return remainingCount <= 5 && !isLoadingMore - } -} diff --git a/crush/Crush/Src/Modules/Login/CLLoginMainController.swift b/crush/Crush/Src/Modules/Login/CLLoginMainController.swift deleted file mode 100644 index bfaa394..0000000 --- a/crush/Crush/Src/Modules/Login/CLLoginMainController.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// CLLoginMainController.swift -// Crush -// -// Created by Leon on 2025/7/17. -// - -import RswiftResources -import UIKit - -class CLLoginMainController: CLViewController { - var viewModel: AccountAuthViewModel = AccountAuthViewModel() - - lazy var appleLogin = SignInAppleLogin() - - override class var shouldPresentThisVc: Bool { - return true - } - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupData() - setupEvent() - } - - private func setupViews() { - navigationView.bgView.backgroundColor = .clear - } - - private func setupData() { - } - - private func setupEvent() { - container.button.addTarget(self, action: #selector(tapOperateButton), for: .touchUpInside) - viewModel.authProcessAction = {[weak self] code, type in - self?.thirdLogin(code: code, type: type) - } - } - - @objc private func tapOperateButton() { - // Sheet - let sheet = EGActionSheet() - sheet.title = "Continue with" - - let icon1 = MWIconFont.image(fromIcon: .socialDiscord, size: .init(width: 24, height: 24), color: .white) - let action1 = EGActionSheetAction(title: "Discord", iconLeading: icon1, autoDismiss: true) { [weak self] in - self?.signInWithDiscord() - } - - let icon2 = MWIconFont.image(fromIcon: .socialGoogle, size: .init(width: 24, height: 24), color: .white) - let action2 = EGActionSheetAction(title: "Google", iconLeading: icon2, autoDismiss: true) { [weak self] in - self?.signInWithGoogle() - } - - let icon3 = MWIconFont.image(fromIcon: .socialApple, size: .init(width: 24, height: 24), color: .white) - let action3 = EGActionSheetAction(title: "Apple", iconLeading: icon3, autoDismiss: true) { [weak self] in - self?.signInWithApple() - } - sheet.addAction(action1) - sheet.addAction(action2) - sheet.addAction(action3) - sheet.show() - } - - // MARK: - Functions - private func thirdLogin(code:String?, type: String?){ - viewModel.thirdLogin(code: code, type: type) {[weak self] state, token in - if(state){ - self?.loadUserSelfInfo(userToken: token) - } - } - } - - private func loadUserSelfInfo(userToken: String){ - UserProvider.request(.userInfoSelfGet, modelType: User.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case let .success(userContent): - guard let user = userContent else { return } - UserCore.shared.updateLocalUser(userNew: user) - NotificationCenter.post(name: .userLoginSuccess) - NotificationCenter.post(name: .userInfoUpdated) - if user.cpUserInfo { - self?.goPersonalInformationFill(userToken) - } else { - self?.close() - } - case .failure: - break - } - } - } - - // MARK: - Actions - - private func signInWithDiscord() { - let vc = DiscordAuthController() - vc.viewModel = viewModel - navigationController?.pushViewController(vc, animated: true) - } - - private func signInWithGoogle() { -// testLogin() - //goPersonalInformationFill("XXXXX") - #if DEBUG - testLogin() - #endif - } - - private func signInWithApple() { - appleLogin.loginByApple() - appleLogin.delegate = self - } - - private func testLogin() { - let token = "thirdToken_example" - - Hud.showIndicator() - let params = ["thirdType": ThirdType.GOOGLE.rawValue, "thirdToken": token] - LoginProvider.request(LoginAPI.loginByThird(params: params), modelType: ThirdLoginResponse.self) { [weak self] result in - switch result { - case let .success(content): - dlog("mock user: \(String(describing: content)), user's token: \(String(describing: content?.token))") - guard let userToken = content?.token else { return } - UserCore.shared.updateToken(tokenNew: userToken) - - UserProvider.request(.userInfoSelfGet, modelType: User.self) { result in - Hud.hideIndicator() - switch result { - case let .success(userContent): - guard let user = userContent else { return } - UserCore.shared.updateLocalUser(userNew: user) - if user.cpUserInfo { - self?.goPersonalInformationFill(userToken) - } else { - self?.close() - } - case .failure: - return - } - } - case .failure: - Hud.hideIndicator() - break - } - } - } - - // MARK: - Helper - - private func goPersonalInformationFill(_ userToken: String) { - let vc = PersonalInformationFillController() - vc.userToken = userToken - navigationController?.pushViewController(vc, animated: true) - } -} - -extension CLLoginMainController: SignInAppleLoginDelegate{ - func signInAppleAuth(token: String, userId: String, name: String) { - thirdLogin(code: token, type: "APPLE") - } -} - diff --git a/crush/Crush/Src/Modules/Login/DiscordAuthController.swift b/crush/Crush/Src/Modules/Login/DiscordAuthController.swift deleted file mode 100644 index 6899259..0000000 --- a/crush/Crush/Src/Modules/Login/DiscordAuthController.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// DiscordAuthController.swift -// Crush -// -// Created by Leon on 2025/8/4. -// - -import UIKit -import WebKit - -class DiscordAuthController: H5BaseViewController { - // MARK: - Properties - - weak var viewModel: AccountAuthViewModel? - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - navigationView.title = "Sign In" - - requestDiscordOAuth() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - UIWindow.applicationKey?.hideAllToasts() - } - - // MARK: - Public Methods - - func requestDiscordOAuth() { - let clientId = AppConst.discordOAuthAppId - let redirectUri = AppConst.discordCallbackUrl - //let urlString = "https://discord.com/api/oauth2/authorize?client_id=\(clientId)&redirect_uri=\(redirectUri)/link/auth&response_type=code&scope=identify" - // Test use. // use %20email - let urlString = "https://discord.com/api/oauth2/authorize?client_id=\(clientId)&redirect_uri=\(redirectUri)/api/auth/discord/callback&response_type=code&scope=identify%20email" - - targetUrl = URL(string: urlString) - } - - // MARK: - Override - - override func tapNaviBackBtn(){ - close() - } - - // MARK: - WKNavigationDelegate - - override func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - guard let urlString = navigationAction.request.url?.absoluteString.removingPercentEncoding else { - decisionHandler(.allow) - return - } - - dlog("Discord urlString: \(urlString)") - - if urlString.contains("link/auth?code=") { - // Handle OAuth code - let components = urlString.components(separatedBy: "link/auth?code=") - if components.count == 2, let code = components.last { - codeToToken(code) - decisionHandler(.cancel) - return - } - } else if urlString.contains("callback?code=") { - let components = urlString.components(separatedBy: "callback?code=") - if components.count == 2, let code = components.last { - codeToToken(code) - decisionHandler(.cancel) - return - } - } - else if urlString.contains("?error=access_denied") { - // Handle cancellation - decisionHandler(.cancel) - navigationController?.popViewController(animated: true) - return - } - - // Allow other navigations - decisionHandler(.allow) - } - - // MARK: - Private Methods - - private func codeToToken(_ code: String) { - guard !code.isEmpty else { return } - - if let authProcessAction = viewModel?.authProcessAction { - UIWindow.applicationKey?.hideAllToasts() - authProcessAction(code, "DISCORD") - navigationController?.popViewController(animated: true) - } - } - - func leaveAndPostNoti() { - UIWindow.applicationKey?.hideAllToasts() - - NotificationCenter.post(name: .userLoginSuccess) - NotificationCenter.post(name: .userInfoUpdated) - - dismiss(animated: true, completion: nil) - } -} diff --git a/crush/Crush/Src/Modules/Login/GenderSelectView.swift b/crush/Crush/Src/Modules/Login/GenderSelectView.swift deleted file mode 100644 index 8463d05..0000000 --- a/crush/Crush/Src/Modules/Login/GenderSelectView.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// GenderSelectView.swift -// Crush -// -// Created by Leon on 2025/7/18. -// -import Combine -import UIKit - -class GenderSelectView: UIView { - var titleLabel: UILabel! - var stackV: UIStackView! - var stackH: UIStackView! - var supportLabel: UILabel! - - var maleBtn: EPChipCenterIconButton! - var femaleBtn: EPChipCenterIconButton! - var unknowBtn: EPChipCenterIconButton! - - @Published var sex: Sex? - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupEvents() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - private func setupViews() { - titleLabel = UILabel() - titleLabel.font = CLSystemToken.typography(token: .tlm).font! - titleLabel.textColor = .c.ctpn - addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.top.leading.equalToSuperview() - } - - stackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 12 - v.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.bottom.equalToSuperview() - } - return v - }() - - stackH = { - let v = UIStackView() - v.spacing = 12 - v.distribution = .fillEqually - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(48) - make.leading.trailing.equalToSuperview() - } - return v - }() - - maleBtn = { - let v = EPChipCenterIconButton() - v.addTarget(self, action: #selector(tapBtn(sender:)), for: .touchUpInside) - v.iconCode = IconCode.male - v.isSelected = false - stackH.addArrangedSubview(v) - return v - }() - - femaleBtn = { - let v = EPChipCenterIconButton() - v.addTarget(self, action: #selector(tapBtn(sender:)), for: .touchUpInside) - v.iconCode = IconCode.female - stackH.addArrangedSubview(v) - return v - }() - - unknowBtn = { - let v = EPChipCenterIconButton() - v.addTarget(self, action: #selector(tapBtn(sender:)), for: .touchUpInside) - v.iconCode = IconCode.nonconforming - v.isSelected = false - stackH.addArrangedSubview(v) - return v - }() - - supportLabel = { - let v = UILabel() - v.font = CLSystemToken.font(token: .tbs) - v.textColor = .c.ctsn - v.numberOfLines = 0 - v.isHidden = true - stackV.addArrangedSubview(v) - return v - }() - - titleLabel.text = "Sex" - } - - private func setupEvents() { - $sex - .sink { [weak self] newSex in - self?.resetSelectState() - // 在此响应 sex 的变化 - dlog("性别变化为: \(String(describing: newSex))") - switch newSex { - case .female: - self?.femaleBtn.isSelected = true - case .male: - self?.maleBtn.isSelected = true - case .noncomfirming: - self?.unknowBtn.isSelected = true - default: break - } - - // 你可以在这里做任何后续处理 - } - .store(in: &cancellables) - } - - private func resetSelectState() { - femaleBtn.isSelected = false - maleBtn.isSelected = false - unknowBtn.isSelected = false - } - - @objc private func tapBtn(sender: UIButton) { - if sender == maleBtn { - sex = .male - } else if sender == femaleBtn { - sex = .female - } else if sender == unknowBtn { - sex = .noncomfirming - } else { - sex = nil - } - // sender.isSelected = true - } -} diff --git a/crush/Crush/Src/Modules/Login/PersonalInformationFillController.swift b/crush/Crush/Src/Modules/Login/PersonalInformationFillController.swift deleted file mode 100644 index a570db8..0000000 --- a/crush/Crush/Src/Modules/Login/PersonalInformationFillController.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// PersonalInformationFillController.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -import UIKit -import DateToolsSwift -import Combine -class PersonalInformationFillController: CLViewController { - lazy var birthdayPicker:BirthdayPickerView = BirthdayPickerView() - - var userToken: String? - - // Data - private var cancellables = Set() - @Published var isRegisterEnabled: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - - - setupViews() - - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - // 禁止侧滑返回 - self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true - } - - private func setupViews(){ - navigationView.backButton.isHidden = true - disabledFullScreenPan() - } - - private func setupEvents(){ - container.bottomButton.addTarget(self, action: #selector(tapRegister), for: .touchUpInside) - container.birthdaySelectView.selectBlock = {[weak self] in - self?.selectBirthday() - } - - $isRegisterEnabled.sink {[weak self] enabled in - self?.container.bottomButton.isEnabled = enabled - }.store(in: &cancellables) - -// Publishers.CombineLatest(container.$nickname, container.sexView.$sex).sink{ -// nickname, sex in -// dlog("CombineLatest nickname:\(nickname), sex:\(sex)") -// }.store(in: &cancellables) - - Publishers.CombineLatest3(container.$nickname, container.sexView.$sex, container.$birthdayStr).map{ -// Publishers.CombineLatest(container.$nickname, container.sexView.$sex).map{ - nickname,sex, birthdayStr in - if let nick = nickname, nick.isValidNickname, let rawValue = sex, (Sex(rawValue: rawValue.rawValue) != nil), let birthdayFormat = birthdayStr, !birthdayFormat.isEmpty { - return true - } - return false - }.assign(to: &$isRegisterEnabled) - - } - - private func selectBirthday(){ - if let date = container.birthdayDate{ - birthdayPicker.setupDefaultSelectDate(date) - }else if let dateStr = container.birthdayStr{ - let date = Date(dateString: dateStr, format: "yyyy-MM-dd") - birthdayPicker.setupDefaultSelectDate(date) - } - - birthdayPicker.show() - birthdayPicker.tapConfirmAction = {[weak self] date, timestamp in - dlog("birthday date:\(date). timestamp:\(timestamp)") - self?.container.birthdayDate = date - self?.container.birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - } - } - - @objc private func tapRegister(){ -// #warning("test") -// //dismiss(animated: true, completion: nil) -// self.close(dismissFirst: true) -// return - guard let user = UserCore.shared.user, let nickname = container.nickname else{ - return - } - - if let oldNickname = user.nickname, nickname != oldNickname{// 新nickname - Hud.showIndicator() - UserOperator.checkname(name: nickname) {[weak self] nicknameOK in - if nicknameOK{ - self?.doCompleteInfo() - }else{ - Hud.hideIndicator() - } - } - }else{ - Hud.showIndicator() - doCompleteInfo() - } - - - // dlog("当前的sex: \(String(describing: container.sexView.sex?.rawValue))") - } - - private func doCompleteInfo(){ - guard let user = UserCore.shared.user, let nickname = container.nickname, let sex = container.sexView.sex, let birthdayDate = container.birthdayDate else{ - return - } - let birthdayTimeStamp = birthdayDate.timeStamp - var params = [String:Any]() - params.updateValue(user.userId, forKey: "userId") - params.updateValue(nickname, forKey: "nickname") - params.updateValue(sex.rawValue, forKey: "sex") - params.updateValue(birthdayTimeStamp, forKey: "birthday") - - LoginProvider.request(LoginAPI.completeUserInfo(params: params), modelType: User.self) {[weak self] result in - Hud.hideIndicator() - switch result{ - case .success: - UserCore.shared.refreshUserInfo {[weak self] result in - self?.close(dismissFirst: true) - } - case.failure: - break - } - } - } - -} diff --git a/crush/Crush/Src/Modules/Login/PersonalInformationFillViews.swift b/crush/Crush/Src/Modules/Login/PersonalInformationFillViews.swift deleted file mode 100644 index a5f60e6..0000000 --- a/crush/Crush/Src/Modules/Login/PersonalInformationFillViews.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// PersonalInformationFillViews.swift -// Crush -// -// Created by Leon on 2025/7/18. -// -import ActiveLabel -import Combine -class PersonalInformationFillView: CLContainer { - var bottomView: UIView! - var activeLabel: ActiveLabel! - var bottomButton: StyleButton! - - var container: LTScrollContainer! - var titleView: TitleView! - var nicknameTitleField: TitleTextField! - var sexView: GenderSelectView! - var birthdaySelectView: CLSelectView! - - // Data - - @Published var nickname: String? - @Published var birthdayStr: String? - @Published var birthdayDate: Date? - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupEvents() - } - - @MainActor required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bottomView = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - // make.height.equalTo(196) - } - return v - }() - - activeLabel = { - let v = ActiveLabel() - v.textColor = UIColor.c.ctsn - v.font = CLSystemToken.font(token: .tbs) - v.numberOfLines = 0 - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - bottomButton = { - let button = StyleButton(type: .custom) - button.primary(size: .large) - bottomView.addSubview(button) - button.snp.makeConstraints { make in - make.bottom.equalTo(activeLabel.snp.top).offset(-16) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview().offset(16) - } - return button - }() - - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomView.snp.top) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - nicknameTitleField = { - let v = TitleTextField() - v.maxLimit = 20 - v.minLimit = 2 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - sexView = { - let v = GenderSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - birthdaySelectView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - #warning("translate") - titleView.title = "login.personal.information".localized() - - nicknameTitleField.titleLabel.text = "nickname".localized() - nicknameTitleField.placeholder = "Nickname" - - birthdaySelectView.titleLabel.text = "Birthday" - birthdaySelectView.placeholder = "Birthday" - - bottomButton.setTitle("Register", for: .normal) - - let userAgreenment = "" - let privacyPolicy = "" - let full = "By clicking to register, you have read and agree to \(userAgreenment) and \(privacyPolicy)" - let activeType1 = ActiveType.custom(pattern: userAgreenment) - let activeType2 = ActiveType.custom(pattern: privacyPolicy) - activeLabel.text = full - activeLabel.enabledTypes = [activeType1, activeType2] - activeLabel.customColor[activeType1] = .red // .cl.cpvn - activeLabel.customColor[activeType2] = .red // .cl.cpvn - activeLabel.handleCustomTap(for: activeType1) { str in - dlog("tap\(str)") - } - activeLabel.handleCustomTap(for: activeType2) { str in - dlog("tap\(str)") - } - activeLabel.customize { label in - label.configureLinkAttribute = { type, attributes, _ in - var attr = attributes - if type == activeType1 || type == activeType2 { - attr[NSAttributedString.Key.foregroundColor] = UIColor.c.cpvn - } - return attr - } - } - } - - private func setupEvents() { - nicknameTitleField.textfield.textDidChanged = { [weak self] textField in - self?.nickname = textField.text?.trimmed - } - - $birthdayStr.sink {[weak self] str in - self?.birthdaySelectView.contentStr = str - }.store(in: &cancellables) - } -} diff --git a/crush/Crush/Src/Modules/Login/View/CLLoginMainView.swift b/crush/Crush/Src/Modules/Login/View/CLLoginMainView.swift deleted file mode 100644 index 010c19e..0000000 --- a/crush/Crush/Src/Modules/Login/View/CLLoginMainView.swift +++ /dev/null @@ -1,205 +0,0 @@ -// -// CLLoginMainView.swift -// Crush -// -// Created by Leon on 2025/9/29. -// - -import UIKit -class CLLoginMainView: UIView { - var containerView: UIView! - var iv1: UIImageView! - var iv2: UIImageView! - var bgImageViewOverlay: UIImageView! - - var bottomFakeBgView: UIView! - var gradientBottom: UIImageView! - var bottomView: UIView! - var button: StyleButton! - - override init(frame: CGRect) { - super.init(frame: frame) - initialViews() - // 延迟启动动画 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - self.setupAnimations() - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func initialViews() { - let image = UIImage(named: "login_home_bg") - - // 计算适合的图片高度 - let fitImageHeight = image!.size.height * UIScreen.main.bounds.width / image!.size.width - - containerView = { - let container = UIView() - addSubview(container) - container.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(fitImageHeight * 2) - } - return container - }() - - iv1 = { - let iv = UIImageView() - iv.image = image - containerView.addSubview(iv) - iv.snp.makeConstraints { make in - make.left.top.right.equalToSuperview() - make.height.equalTo(fitImageHeight) - } - return iv - }() - - iv2 = { - let iv = UIImageView() - iv.image = image - containerView.addSubview(iv) - iv.snp.makeConstraints { make in - make.left.right.bottom.equalToSuperview() - make.top.equalTo(iv1.snp.bottom) - make.height.equalTo(fitImageHeight) - } - return iv - }() - - bgImageViewOverlay = { - let v = UIImageView() - let image = UIImage(named: "login_home_bg_overlay") - v.image = image - addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(600/393.0) - } - return v - }() - - gradientBottom = { - let iv = UIImageView() - iv.image = UIImage(named: "login_home_bottom_overlay") - addSubview(iv) - iv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - make.height.equalTo(iv.snp.width).multipliedBy(360/393.0) - } - return iv - }() - - bottomFakeBgView = { - let v = UIView() - v.backgroundColor = .c.cbd - insertSubview(v, belowSubview: gradientBottom) - v.snp.makeConstraints { make in - make.bottom.leading.trailing.equalToSuperview() - //make.height.equalTo(218+UIWindow.safeAreaBottom) - make.top.equalTo(bgImageViewOverlay.snp.bottom) - } - return v - }() - - bottomView = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - button = { - let button = StyleButton(type: .custom) - button.primary(size: .large) - - let localizedString = "Login or Signup" - // let localizedString = NSLocalizedString("welcomeWithName", comment: "") - button.setTitle(localizedString, for: .normal) - - bottomView.addSubview(button) - button.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(48) - make.trailing.equalToSuperview().offset(-48) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom - 16) - make.top.equalToSuperview().offset(24) - } - return button - }() - - let subTitle: UILabel = { - let label = UILabel() - label.font = CLSystemToken.font(token: .tll) - label.textColor = .c.ctpn - label.textAlignment = .center - label.numberOfLines = 0 - bottomView.addSubview(label) - label.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(48) - make.trailing.equalToSuperview().offset(-48) - make.bottom.equalTo(button.snp.top).offset(-72) - } - return label - }() - subTitle.text = "From 'Crush' to 'I Do', Sparked by every chat" - - // "Chat, Crush, AI Date" - -// let titleLabel: UILabel = { -// let label = UILabel() -// label.font = CLSystemToken.font(token: .tdl) -// label.textColor = .c.ctpn -// label.textAlignment = .center -// bottomView.addSubview(label) -// label.snp.makeConstraints { make in -// make.leading.equalToSuperview().offset(48) -// make.trailing.equalToSuperview().offset(-48) -// make.bottom.equalTo(subTitle.snp.top).offset(-24) -// } -// return label -// }() -// titleLabel.text = R.string.localizable.crushLevel() - - let logo = { - let v = UIImageView() - v.image = UIImage(named: "cl_logo") - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalTo(subTitle.snp.top).offset(-24) - } - return v - }() - logo.isHidden = false - } - - func setupAnimations() { - let canOff = iv1.frame.height - containerView.layer.removeAllAnimations() - - let animation = CABasicAnimation(keyPath: "position.y") - animation.byValue = -canOff - animation.duration = 20.0 - animation.isRemovedOnCompletion = false - animation.repeatCount = Float(CGFloat.greatestFiniteMagnitude) - containerView.layer.add(animation, forKey: "positionAnimation") - } - - func stopAnimation() { - containerView.layer.removeAllAnimations() - } - - func startAnimation() { - setupAnimations() - } - - deinit { - stopAnimation() - } -} diff --git a/crush/Crush/Src/Modules/Login/ViewModel/AccountAuthViewModel.swift b/crush/Crush/Src/Modules/Login/ViewModel/AccountAuthViewModel.swift deleted file mode 100644 index e12555d..0000000 --- a/crush/Crush/Src/Modules/Login/ViewModel/AccountAuthViewModel.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// AccountAuthViewModel.swift -// Crush -// -// Created by Leon on 2025/8/4. -// - -import Foundation - -class AccountAuthViewModel{ - var authProcessAction: ((_ code:String , _ type: String ) -> Void)? - - func thirdLogin(code: String?, type: String?, _ completion:((_ state:Bool, _ token: String)->Void)?){ - guard let thirdCode = code, let thirdType = type else { - assert(false) - return - } - Hud.showIndicator() - let params = ["thirdType":thirdType, "thirdToken": thirdCode] - LoginProvider.request(LoginAPI.loginByThird(params: params), modelType: ThirdLoginResponse.self) { [weak self] result in - switch result { - case let .success(content): - // dlog("mock user: \(String(describing: content)), user's token: \(String(describing: content?.token))") - guard let userToken = content?.token else { return } - UserCore.shared.updateToken(tokenNew: userToken) - - completion?(true, userToken) - case .failure: - Hud.hideIndicator() - completion?(false, "") - break - } - } - } -} diff --git a/crush/Crush/Src/Modules/Login/ViewModel/SignInAppleLogin.swift b/crush/Crush/Src/Modules/Login/ViewModel/SignInAppleLogin.swift deleted file mode 100755 index 7083f50..0000000 --- a/crush/Crush/Src/Modules/Login/ViewModel/SignInAppleLogin.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// SignInAppleLogin.swift -// E-Wow -// -// Created by dong on 2021/1/6. -// - -import AuthenticationServices -import UIKit - -protocol SignInAppleLoginDelegate: NSObjectProtocol { - func signInAppleAuth(token: String, userId: String, name: String) -} - - -@available(iOS 13.0, *) -class SignInAppleLogin: NSObject { - var appleIDProvider: ASAuthorizationAppleIDProvider! - weak var delegate: SignInAppleLoginDelegate? - - override init() { - super.init() - - appleIDProvider = ASAuthorizationAppleIDProvider() - NotificationCenter.default.addObserver(self, selector: #selector(notiSignWithAppleIDStateChanged(noti:)), name: NSNotification.Name(ASAuthorizationAppleIDProvider.credentialRevokedNotification.rawValue), object: nil) - } - - func loginByApple() { - let request = appleIDProvider.createRequest() - request.requestedScopes = [.fullName, .email] - // fix cancel animation: https://developer.apple.com/forums/thread/683981 - // let vc = ASAuthorizationController(authorizationRequests: [request, ASAuthorizationPasswordProvider().createRequest()]) - let vc = ASAuthorizationController(authorizationRequests: [request]) - vc.presentationContextProvider = self - vc.delegate = self - vc.performRequests() - } - - // MARK: noti - - @objc func notiSignWithAppleIDStateChanged(noti: Notification) { - dlog("apple id state chagned: \(noti.name) \(noti.userInfo?.description ?? "")") - } - - deinit { - NotificationCenter.default.removeObserver(self) - } -} - -@available(iOS 13.0, *) -extension SignInAppleLogin: ASAuthorizationControllerDelegate { - // 🔥🔥🔥 - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - if authorization.credential.isKind(of: ASAuthorizationAppleIDCredential.classForCoder()) { - // 用户登录使用ASAuthorizationAppleIDCredential - let appleIDCredential = authorization.credential as! ASAuthorizationAppleIDCredential - let userId = appleIDCredential.user - // 使用过授权的,可能获取不到以下三个参数 - _ = appleIDCredential.fullName?.familyName ?? "" - _ = appleIDCredential.fullName?.givenName ?? "" - _ = appleIDCredential.email ?? "" - - _ = appleIDCredential.authorizationCode ?? Data() - // 用于判断当前登录的苹果账号是否是一个真实用户,取值有:unsupported、unknown、likelyReal - // let realUserStatus = appleIDCredential.realUserStatus - - guard let identityToken = appleIDCredential.identityToken, let tokenStr = String(data: identityToken, encoding: .utf8) else { - return - } - dlog("tokenStr: \(tokenStr), name: \(appleIDCredential.fullName?.givenName ?? "")") - - delegate?.signInAppleAuth(token: tokenStr, userId: userId, name:appleIDCredential.fullName?.givenName ?? "") - - // 服务器验证需要使用的参数 - } else if authorization.credential.isKind(of: ASPasswordCredential.classForCoder()) { - // 这个获取的是iCloud记录的账号密码,需要输入框支持iOS 12 记录账号密码的新特性,如果不支持,可以忽略 - // Sign in using an existing iCloud Keychain credential. - // 用户登录使用现有的密码凭证 - let passworCreddential = authorization.credential as! ASPasswordCredential - // 密码凭证对象的用户标识 用户的唯一标识 - _ = passworCreddential.user - // 密码凭证对象的密码 - _ = passworCreddential.password - } else { - // "授权信息不符合" - } - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - dlog("❌ apple auth error : \(error.localizedDescription)") - } -} - -@available(iOS 13.0, *) -extension SignInAppleLogin: ASAuthorizationControllerPresentationContextProviding { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - // return UIWindow.key! - return UIWindow.applicationKey ?? ASPresentationAnchor() - } -} diff --git a/crush/Crush/Src/Modules/Me/Creator/CreatorIntroductionController.swift b/crush/Crush/Src/Modules/Me/Creator/CreatorIntroductionController.swift deleted file mode 100644 index 02601e0..0000000 --- a/crush/Crush/Src/Modules/Me/Creator/CreatorIntroductionController.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// CreatorIntroductionController.swift -// Crush -// -// Created by Leon on 2025/9/19. -// - -import UIKit - -class CreatorIntroductionController: CLBaseViewController { - var scrollContainer: LTScrollContainer! - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - } - - private func setupViews() { - let title = "Creator" - navigationView.alpha0Title = title - - scrollContainer = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.scrollView.delegate = self - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - return v - }() - - do { - let container = UIView() - scrollContainer.stack.addArrangedSubview(container) - container.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - - let bg = { - let v = UIImageView() - v.image = UIImage(named: "creator_star_bg") - v.contentMode = .scaleAspectFill - container.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - bg.isHidden = false - - let titleLabel = { - let v = LineSpaceLabel() - v.textAlignment = .center - let typo = CLSystemToken.typography(token: .thm) - v.config(typo) - container.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-24) - } - return v - }() - - do { - let item = createItemView(iconLeft: true, icon: UIImage(named: "creator-gift"), title: "Gift Revenue", desc: "The interlocutor can pay to send virtual gifts to your AI character.") - scrollContainer.stack.addArrangedSubview(item) - } - do { - let item = createItemView(iconLeft: false, icon: UIImage(named: "creator-pic"), title: "Image Sales", desc: "The interlocutor can pay to unlock the album pictures you created for the AI character.") - scrollContainer.stack.addArrangedSubview(item) - } - do { - let item = createItemView(iconLeft: true, icon: UIImage(named: "creator-diamond"), title: "More Revenue", desc: "Stay tuned") - scrollContainer.stack.addArrangedSubview(item) - } - titleLabel.text = "Create high-quality AI characters and win CrushCoin." - } - } - - private func createItemView(iconLeft: Bool, icon: UIImage?, title: String, desc: String) -> UIView { - let container = UIView() - - let stackH = { - let v = UIStackView() - v.spacing = 16 - v.axis = .horizontal - v.distribution = .fillEqually - v.alignment = .center - container.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(8) - make.bottom.equalToSuperview().offset(-8) - } - return v - }() - - let wordsStackV = { - let v = UIStackView() - v.spacing = 4 - v.axis = .vertical - v.alignment = .leading - stackH.addArrangedSubview(v) - return v - }() - - let titleLabel = { - let v = CLLabel() - v.numberOfLines = 0 - v.font = .t.tts - wordsStackV.addArrangedSubview(v) - return v - }() - - let descLabel = { - let v = CLLabel() - v.numberOfLines = 0 - v.font = .t.tbs - v.textColor = .c.ctsn - wordsStackV.addArrangedSubview(v) - return v - }() - - let iconView = { - let v = UIImageView() - v.backgroundColor = .c.cbdi - v.cornerRadius = 16 - if iconLeft { - stackH.insertArrangedSubview(v, at: 0) - } else { - stackH.addArrangedSubview(v) - } - v.snp.makeConstraints { make in - make.height.equalTo(v.snp.width).multipliedBy(107 / 160.0) - } - return v - }() - - titleLabel.text = title - descLabel.text = desc - iconView.image = icon - - return container - } -} - -extension CreatorIntroductionController: UIScrollViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} diff --git a/crush/Crush/Src/Modules/Me/MeRootPageController.swift b/crush/Crush/Src/Modules/Me/MeRootPageController.swift deleted file mode 100644 index a712e6b..0000000 --- a/crush/Crush/Src/Modules/Me/MeRootPageController.swift +++ /dev/null @@ -1,258 +0,0 @@ -// -// MeRootPageController.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit -import TZImagePickerController -class MeRootPageController: CLTabRootController { - var settingButton: EPIconGhostButton! - - private var lastRequestDate: Date? - - var uploadingAvatar : UploadPhotoM? - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDats() - setupEvents() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - controlRequestUserInfo() - } - - private func controlRequestUserInfo() { - let now = Date() - if let lastDate = lastRequestDate { - let interval = now.timeIntervalSince(lastDate) - if interval < 30 { - //30s - return - } - } - - // 超过30秒或第一次出现,可以请求 - loadUserInfo() - loadAIRoleList() - - lastRequestDate = now - } - - private func setupViews() { - navigationView.bgView.alpha = 0 - - // title = nickname - settingButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .iconSetting) - v.addTarget(self, action: #selector(settingAction), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 44, height: 44)) - } - return v - }() - - #if DEBUG - if APIConfig.environment == .test{ - let debugButton = { - let v = UIButton() - v.setTitle("Debug Entrances", for: .normal) - v.titleLabel?.font = .t.tlm - v.setTitleColor(.yellow, for: .normal) - v.layer.borderColor = UIColor.yellow.cgColor - v.layer.borderWidth = 1 - v.addTarget(self, action: #selector(tapDebugButton), for: .touchUpInside) - navigationView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.height.equalTo(30) - make.centerY.equalTo(navigationView.backButton) - } - return v - }() - debugButton.isHidden = false - } - #endif - } - - private func setupDats() { - // See controlRequestUserInfo - } - - private func setupEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(notiAIRoleAddOrDelete), name: AppNotificationName.aiRoleCreatedOrDelete.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notificationAIRoleInfoUpdated(noti:)), name: AppNotificationName.aiRoleInfoChanged.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(logOut), name: AppNotificationName.userLogout.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(login), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyUserInfoChanged), name: AppNotificationName.userInfoUpdated.notificationName, object: nil) - - //container.headerView.avatarTopButton.addTarget(self, action: #selector(tapAvatarTopButton), for: .touchUpInside) - container.tapAvatarAction = {[weak self] in - self?.tapAvatarTopButton() - } - } - - private func loadUserInfo() { - UserCore.shared.refreshUserInfo { _ in - } - } - - private func loadAIRoleList() { - AIRoleProvider.request(.aiRoleListOfMine, modelType: [AIRoleInfo].self) { [weak self] result in - switch result { - case let .success(success): - self?.container.config(datas: success) - // self?.container.headerView.setupRoleCount(valid: success?.count ?? 0, total: 10) - case let .failure(failure): - dlog(failure) - } - } - } - - // MARK: - Events - - @objc private func settingAction() { - let vc = SettingListController() - navigationController?.pushViewController(vc, animated: true) - } - - @objc private func tapDebugButton() { - UIWindow.getTopViewController()?.navigationController?.pushViewController(TestEntrancesController(), animated: true) - } - - @objc private func tapAvatarTopButton(){ - guard let picker = ImagePicker(maxImagesCount: 1, delegate: self) else { return } - picker.allowPickingGif = false - picker.showSelectBtn = false - picker.needCircleCrop = true - - picker.cropRect = CGRect(origin: .init(x: 0, y: 0), size: CGSize(width: UIScreen.width, height: UIScreen.height)) - picker.circleCropRadius = Int((UIScreen.width - 37*2) * 0.5) - present(picker, animated: true, completion: nil) - } - - // MARK: - Functions - - func updateHeadImage(headUrlString: String?, completion: ((Bool)->Void)? = nil){ - guard let avatar = headUrlString else { return } - var params = [String:Any]() - params.updateValue(UserCore.shared.user!.userId, forKey: "userId") - params.updateValue(avatar, forKey: "headImage") - Hud.showIndicator() - UserProvider.request(.userInfoEdit(params: params), modelType: String.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.uploadingAvatar = nil - UserCore.shared.refreshUserInfo(block: nil) - completion?(true) - case .failure: - completion?(false) - } - } - } - - // MARK: - Notification - - @objc private func notiAIRoleAddOrDelete(){ - loadAIRoleList() - } - - @objc private func notificationAIRoleInfoUpdated(noti: Notification) { - loadAIRoleList() - } - - @objc private func notifyUserInfoChanged(){ - - } - - @objc private func logOut(){ - container.config(datas: []) - } - - @objc private func login(){ - loadAIRoleList() - } -} - -extension MeRootPageController : TZImagePickerControllerDelegate{ - func imagePickerController(_: TZImagePickerController!, - didFinishPickingPhotos photos: [UIImage]!, - sourceAssets _: [Any]!, - isSelectOriginalPhoto _: Bool, - infos _: [[AnyHashable: Any]]!) - { - guard let img = photos.first else { - return - } - - // Crop Avatar - let cropViewController = CropViewController(image: img) { [unowned self] croppedImage in - - self.container.headerView.avatarView.bindImage(img: croppedImage, showUploading: true) - - // 裁剪后的图片 - let photo = UploadPhotoM() - photo.image = croppedImage - photo.imageSize = croppedImage?.size ?? CGSizeZero - photo.addThisItemTimeStamp = Date().timeStamp - photo.delegate = self - uploadingAvatar = photo - - CloudStorage.shared.s3AddPhotos([photo], bucket: .HEAD_IMAGE) - } - let navc = CLNavigationController(rootViewController: cropViewController) - UIWindow.getTopViewController()?.present(navc, animated: true, completion: nil) - -// let model = UploadPhotoM() -// model.image = img -// model.imageSize = img.size -// model.addThisItemTimeStamp = Date().timeStamp -// model.delegate = self -// uploadingAvatar = model -// -// self.container.headerView.avatarView.bindImage(img: img, showUploading: true) -// -// CloudStorage.shared.s3AddPhotos([model], bucket: .HEAD_IMAGE) - - - } -} - -extension MeRootPageController:UploadPhotoDelegate{ - func uploadFailWith(_ photo: UploadPhotoM) { - dlog("👮upload failed: \(photo)") - self.container.headerView.avatarView.bindImage(img: nil) - } - - func uploadProgress(_ progress: Float) { - dlog("👮progress \(progress)") - } - - func uploadDoneWith(_ photo: UploadPhotoM){ - dlog("👮上传成功: \(photo.remoteFullPath ?? ""), remoteImageUrlString \(photo.remoteImageUrlString ?? "")") - } - - func imageCheck(_ photoM: UploadPhotoM, result: Bool){ - dlog("👮鉴黄结果:\(result)") - - if(result){ - // self.headView.bindImageUrl(imgUrl: photoM.remoteFullPath) - // 上传到s3... 并修改个人信息 - self.updateHeadImage(headUrlString: photoM.remoteFullPath) {[weak self] result in - if result{ - self?.container.headerView.avatarView.bindImageUrl(imgUrl: photoM.remoteFullPath) - } - } - }else{ - self.container.headerView.avatarView.bindImageUrl(imgUrl: UserCore.shared.user?.headImage) - } - - } -} diff --git a/crush/Crush/Src/Modules/Me/Setting/AboutUsController.swift b/crush/Crush/Src/Modules/Me/Setting/AboutUsController.swift deleted file mode 100644 index 10e7de1..0000000 --- a/crush/Crush/Src/Modules/Me/Setting/AboutUsController.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AboutUsController.swift -// Crush -// -// Created by Leon on 2025/9/30. -// - -import UIKit - -class AboutUsController: CLBaseViewController { - var scrollContainer: LTScrollContainer! - var titleView: TitleView! - - var headIcon: AutoRatioImageView! - var content1Label: LineSpaceLabel! - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - } - - private func setupViews() { - let title = "About Us" - - navigationView.alpha0Title = title - - scrollContainer = { - let v = LTScrollContainer() - v.stack.spacing = 16 - v.scrollView.delegate = self - view.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(navigationView.snp.bottom) - make.leading.trailing.bottom.equalToSuperview() - } - return v - }() - - titleView = { - let v = TitleView() - v.title = title - scrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - headIcon = { - let v = AutoRatioImageView() - v.setImage(UIImage(named: "about_us_head_logo")) - scrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - scrollContainer.stack.setCustomSpacing(24, after: headIcon) - - content1Label = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbm) - v.config(typo) - scrollContainer.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - let content = "Grow your love story with CrushLevel AI—From ‘Hi’ to ‘I Do', sparked by every chat\n\nAt CrushLevel AI, every chat writes a new verse in your love epic—From that tentative \"Hi\" to the trembling \"I do\",find a home for the flirts you never sent,the responses you longed for,and the risky emotional gambles you feared to take.\n\nContact Us: support@crushlevel.ai" - content1Label.text = content - } -} - -extension AboutUsController: UIScrollViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} diff --git a/crush/Crush/Src/Modules/Me/Setting/AccountManageController.swift b/crush/Crush/Src/Modules/Me/Setting/AccountManageController.swift deleted file mode 100644 index 59e56cc..0000000 --- a/crush/Crush/Src/Modules/Me/Setting/AccountManageController.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// AccountManageController.swift -// Crush -// -// Created by Leon on 2025/7/23. -// - -import UIKit - -class AccountManageController: CLBaseViewController { - var deleteAccountButton: StyleButton! - var titleView: TitleView! - - var thirdAccountBlock: UIView! - var thirdAccountIcon: UIImageView! - var thirdStackV : UIStackView! - var thirdAccountLabel: UILabel! - var thirdEmailLabel : UILabel! - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - let title = "Account Manage" - navigationView.alpha0Title = title - - deleteAccountButton = { - let v = StyleButton(type: .custom) - v.defaultDestructive(size: .large) - v.addTarget(self, action: #selector(deleteAccountAction), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom * 0.5) - } - v.setTitle("Delete Account", for: .normal) - return v - }() - - titleView = { - let v = TitleView() - v.title = title - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - make.trailing.equalToSuperview() - } - return v - }() - - thirdAccountBlock = { - let v = UIView() - v.backgroundColor = .c.csbn - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.top.equalTo(titleView.snp.bottom).offset(24) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.height.equalTo(80) - } - return v - }() - - thirdAccountIcon = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .socialGoogle, size: CGSize(width: 24, height: 24), color: .c.ctpn) - v.contentMode = .center - v.layer.cornerRadius = 24 - v.layer.masksToBounds = true - v.layer.borderWidth = 1 - v.layer.borderColor = UIColor.c.con.cgColor - thirdAccountBlock.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - make.width.equalTo(48) - make.height.equalTo(48) - } - return v - }() - - thirdStackV = { - let v = UIStackView() - v.axis = .vertical - v.alignment = .leading - v.spacing = 4 - thirdAccountBlock .addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(thirdAccountIcon.snp.trailing).offset(12) - make.trailing.lessThanOrEqualToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - return v - }() - - thirdAccountLabel = { - let v = UILabel() - v.text = "Third Account" - v.font = .t.tts - v.textColor = .c.ctpn - thirdStackV.addArrangedSubview(v) - return v - }() - - thirdEmailLabel = { - let v = UILabel() - v.textColor = .c.ctsn - v.font = .t.tbs - thirdStackV.addArrangedSubview(v) - v.text = "-" - return v - }() - } - - private func setupDats() { - guard let user = UserCore.shared.user else {return} - - let thirdType = user.thirdType ?? .APPLE - switch thirdType { - case .APPLE: - thirdAccountIcon.image = MWIconFont.image(fromIcon: .socialApple, size: CGSize(width: 24, height: 24), color: .c.ctpn) - case .GOOGLE: - thirdAccountIcon.image = MWIconFont.image(fromIcon: .socialGoogle, size: CGSize(width: 24, height: 24), color: .c.ctpn) - case .DISCORD: - thirdAccountIcon.image = MWIconFont.image(fromIcon: .socialDiscord, size: CGSize(width: 24, height: 24), color: .c.ctpn) -// default: -// break - } - - thirdAccountLabel.text = user.thirdNickname ?? "-" - - if let email = user.thirdEmail, email.count > 0{ - thirdEmailLabel.text = email - }else { - thirdEmailLabel.text = "-" - } - - } - - private func setupEvents() { - } - - @objc func deleteAccountAction() { - let alert = Alert(title: "Delete Account", text: "Are you sure you want to delete your account?") - let action1 = AlertAction(title: "Delete", actionStyle: .destructive) {[weak self] in - self?.doDeleteAccount() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) { - } - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } - - private func doDeleteAccount(){ -// #warning("test") -// Hud.toast(str: "账号删除成功") { _ in -// UserCore.shared.logout() -// AppRouter.goBackRootController { -// //NotificationCenter.post(name: .presentSignInVc, object: nil, userInfo: nil) -// } -// } - - Hud.showIndicator() - UserProvider.request(UserAPI.userDel) { result in - Hud.hideIndicator() - switch result{ - case .success: - Hud.toast(str: "账号删除成功") { _ in - UserCore.shared.logout() - AppRouter.goBackRootController { - - } - } - case .failure: - return - } - } - } -} diff --git a/crush/Crush/Src/Modules/Me/Setting/PersonInformationEditController.swift b/crush/Crush/Src/Modules/Me/Setting/PersonInformationEditController.swift deleted file mode 100644 index 358b23c..0000000 --- a/crush/Crush/Src/Modules/Me/Setting/PersonInformationEditController.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// PersonInformationEditController.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import UIKit -import Combine - -class PersonInformationEditController: CLViewController { - lazy var birthdayPicker:BirthdayPickerView = BirthdayPickerView() - - @Published var buttonEnable:Bool = false - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - - } - - private func setupDats(){ - - } - - private func setupEvents(){ - container.bottomButton.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - - $buttonEnable.sink {[weak self] enable in - self?.container.bottomButton.isEnabled = enable; - }.store(in: &cancellables) - - Publishers.CombineLatest(container.$birthdayDate.prepend(nil), container.$nickname.prepend(nil)).map{ - date, name in - guard let birthdayDate = date, let nickname = name else{ - return false - } - - if Date().years(from: birthdayDate) < 18{ - return false - } - - if !nickname.isValidNickname { - return false - } - - return true - }.assign(to: &$buttonEnable) - } - - @objc func tapBottomButton(){ - guard let user = UserCore.shared.user, let nickname = container.nickname, let birthdayDate = container.birthdayDate else{ - return - } - - let timestamp = birthdayDate.timeStamp - - if let oldNickname = user.nickname, nickname != oldNickname{ - Hud.showIndicator() - UserOperator.checkname(name: nickname) {[weak self] nicknameOK in - if nicknameOK{ - self?.doModify(userId: user.userId, nickname: nickname, timestamp: timestamp) - }else{ - Hud.hideIndicator() - } - } - }else{ - Hud.showIndicator() - doModify(userId: user.userId, nickname: nickname, timestamp: timestamp) - } - } - - - private func doModify(userId: Int, nickname:String, timestamp: Int){ - var params = [String:Any]() - params.updateValue(userId, forKey: "userId") - params.updateValue(nickname, forKey: "nickname") - params.updateValue(timestamp, forKey: "birthday") - - - UserProvider.request(UserAPI.userInfoEdit(params: params), modelType: Bool.self) {[weak self] result in - Hud.hideIndicator() - switch result{ - case .success: - UserCore.shared.user?.nickname = nickname - UserCore.shared.user?.birthday = Int64(timestamp) - NotificationCenter.post(name: .userInfoUpdated) - UserCore.shared.refreshUserInfo(block: nil) - self?.close() - case .failure: - break - } - } - } - -} diff --git a/crush/Crush/Src/Modules/Me/Setting/SettingListController.swift b/crush/Crush/Src/Modules/Me/Setting/SettingListController.swift deleted file mode 100644 index b222f52..0000000 --- a/crush/Crush/Src/Modules/Me/Setting/SettingListController.swift +++ /dev/null @@ -1,461 +0,0 @@ -// -// SettingListController.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit -import TZImagePickerController -import Combine - -struct SettingItem { - let title: String - let action: (() -> Void)? -} - -class SettingViewModel { - var itemsSection1: [SettingItem] - - var itemsSection2:[SettingItem] - - init(itemsSection1: [SettingItem], itemsSection2: [SettingItem]) { - self.itemsSection1 = itemsSection1 - self.itemsSection2 = itemsSection2 - } -} - -class SettingListController: CLBaseViewController { - let logoutButton = StyleButton() - let tableView = UITableView() - - var viewModel: SettingViewModel! - //var titleView: TitleView! - - var headView: SettingHeaderView! - - var uploadingAvatar : UploadPhotoM? - - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - viewModel = SettingViewModel(itemsSection1: [ - SettingItem(title: "Edit Profile", action: { [weak self] in self?.goPersonalInfoEdit()}), - SettingItem(title: "Account", action: { [weak self] in self?.goAccountManage() }), - ], itemsSection2: [ - SettingItem(title: "About Us", action: { AppRouter.goAboutUs() }), - SettingItem(title: "User Agreement", action: { AppRouter.goTermsOfService() }), - SettingItem(title: "Privacy Policy", action: { AppRouter.goPrivacyPolicy() }), - ]) - - setupViews() - setupEvents() - navigationView.alpha0Title = "Setting" - } - - private func setupViews() { - logoutButton.tertiary(size: .large) - logoutButton.setTitle("Logout", for: .normal) - logoutButton.addTarget(self, action: #selector(logoutButtonTapped), for: .touchUpInside) - view.addSubview(logoutButton) - logoutButton.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) - view.addSubview(tableView) - tableView.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - make.bottom.equalTo(logoutButton.snp.top).offset(-16) - } - tableView.separatorStyle = .none - tableView.dataSource = self - tableView.delegate = self - tableView.backgroundColor = .clear - tableView.register(SettingCell.self, forCellReuseIdentifier: "SettingCell") - - headView = { - let v = SettingHeaderView() - return v - }() - - headView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 200) - - tableView.tableHeaderView = headView - headView.heightChangeBlock = {[weak self] height in - self?.headView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: height) - self?.tableView.tableHeaderView = self?.headView - } - } - - private func setupEvents() { - headView.avatarView.setTapAction { [weak self] in - self?.tapAvatarButton() - } - NotificationCenter.default.addObserver(self, selector: #selector(notiUserInfoChanged), name: AppNotificationName.userInfoUpdated.notificationName, object: nil) - - UserCore.shared.$user.sink {[weak self] user in - self?.headView.avatarView.bindImageUrl(imgUrl: user?.headImage) - }.store(in: &cancellables) - } - - @objc func logoutButtonTapped() { - let alert = Alert(title: "Log out", text: "请确认是否退出登录?") - let action1 = AlertAction(title: "Log out", actionStyle: .destructive) {[weak self] in - self?.doLogoutRequest() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert .addAction(action1) - alert.addAction(action2) - alert.show() - } - - // MARK: - Action - @objc func tapAvatarButton(){ - guard let picker = ImagePicker(maxImagesCount: 1, delegate: self) else { return } - picker.allowPickingGif = false - picker.showSelectBtn = false - picker.needCircleCrop = true - - picker.cropRect = CGRect(origin: .init(x: 0, y: 0), size: CGSize(width: UIScreen.width, height: UIScreen.height)) - picker.circleCropRadius = Int((UIScreen.width - 37*2) * 0.5) - present(picker, animated: true, completion: nil) - } - - // MARK: - Helper - - private func doLogoutRequest(){ - Hud.showIndicator() - UserProvider.request(.userSignout) { result in - Hud.hideIndicator() - switch result{ - case .success: - UserCore.shared.logout() - - AppRouter.goBackRootController { - Hud.toast(str: "已退出登陆") - } - case .failure: - return - } - } - } - - private func goAccountManage() { - let vc = AccountManageController() - navigationController?.pushViewController(vc, animated: true) - } - - private func goPersonalInfoEdit(){ - let vc = PersonInformationEditController() - navigationController?.pushViewController(vc, animated: true) - } - - func updateHeadImage(headUrlString: String?, completion: ((Bool)->Void)? = nil){ - guard let avatar = headUrlString else { return } - var params = [String:Any]() - params.updateValue(UserCore.shared.user!.userId, forKey: "userId") - params.updateValue(avatar, forKey: "headImage") - Hud.showIndicator() - UserProvider.request(.userInfoEdit(params: params), modelType: String.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.uploadingAvatar = nil - UserCore.shared.refreshUserInfo(block: nil) - completion?(true) - case .failure: - completion?(false) - } - } - } - - // MARK: - Noti - @objc private func notiUserInfoChanged(){ - self.headView.avatarView.bindImageUrl(imgUrl: UserCore.shared.user?.headImage) - } -} - -extension SettingListController:TZImagePickerControllerDelegate{ - func imagePickerController(_: TZImagePickerController!, - didFinishPickingPhotos photos: [UIImage]!, - sourceAssets _: [Any]!, - isSelectOriginalPhoto _: Bool, - infos _: [[AnyHashable: Any]]!) - { - guard let img = photos.first else { - return - } - - // Crop Avatar - let cropViewController = CropViewController(image: img) { [unowned self] croppedImage in - self.headView.avatarView.bindImage(img: croppedImage, showUploading: true) - - // 裁剪后的图片 - let photo = UploadPhotoM() - photo.image = croppedImage - photo.imageSize = croppedImage?.size ?? CGSizeZero - photo.addThisItemTimeStamp = Date().timeStamp - photo.delegate = self - uploadingAvatar = photo - - CloudStorage.shared.s3AddPhotos([photo], bucket: .HEAD_IMAGE) - } - let navc = CLNavigationController(rootViewController: cropViewController) - UIWindow.getTopViewController()?.present(navc, animated: true, completion: nil) - } -} - -extension SettingListController:UploadPhotoDelegate{ - func uploadFailWith(_ photo: UploadPhotoM) { - dlog("👮upload failed: \(photo)") - self.headView.avatarView.bindImage(img: nil) - } - - func uploadProgress(_ progress: Float) { - dlog("👮progress \(progress)") - } - - func uploadDoneWith(_ photo: UploadPhotoM){ - dlog("👮上传成功: \(photo.remoteFullPath ?? ""), remoteImageUrlString \(photo.remoteImageUrlString ?? "")") - } - - func imageCheck(_ photoM: UploadPhotoM, result: Bool){ - dlog("👮鉴黄结果:\(result)") - - if(result){ - // 上传到s3... 并修改个人信息 - self.updateHeadImage(headUrlString: photoM.remoteFullPath) {[weak self] result in - if result{ - self?.headView.avatarView.bindImageUrl(imgUrl: photoM.remoteFullPath) - } - } - }else{ - self.headView.avatarView.bindImageUrl(imgUrl: UserCore.shared.user?.headImage) - } - - } -} - -extension SettingListController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 2 - } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0{ - return viewModel.itemsSection1.count - }else if section == 1{ - return viewModel.itemsSection2.count - } - return 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - var item:SettingItem - if(indexPath.section == 0){ - item = viewModel.itemsSection1[indexPath.row] - }else{ - item = viewModel.itemsSection2[indexPath.row] - } - let cell = tableView.dequeueReusableCell(withIdentifier: "SettingCell", for: indexPath) as! SettingCell - cell.titleLabel.text = item.title - - let total = self.tableView(tableView, numberOfRowsInSection: indexPath.section) - cell.line.isHidden = true - if total == 1{ - cell.setupFullCorner() - }else if indexPath.row == 0{ - cell.line.isHidden = false - cell.setupFirstCellCorner() - }else if indexPath.row == total - 1{ - cell.setupLastCellCorner() - }else{ - cell.line.isHidden = false - cell.setupEmptyCorner() - } - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - var item:SettingItem - if(indexPath.section == 0){ - item = viewModel.itemsSection1[indexPath.row] - }else{ - item = viewModel.itemsSection2[indexPath.row] - } - item.action?() - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 16 - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let spacer = UIView() - spacer.backgroundColor = .clear - return spacer - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} - - -class SettingHeaderView: UIView{ - var titleView: TitleView! - var avatarView: AvatarView! - - var heightChangeBlock: ((CGFloat) -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - let maxY = avatarView.frame.maxY + 12 - heightChangeBlock?(maxY) - } - - private func setupViews() { - titleView = { - let view = TitleView() - view.title = "Setting" - view.backgroundColor = .clear - addSubview(view) - view.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - } - return view - }() - - avatarView = { - let view = AvatarView() - view.cameraCircle.isHidden = false - addSubview(view) - view.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(titleView.snp.bottom).offset(12) - make.size.equalTo(CGSize(width: 90, height: 90)) - } - return view - }() - } - - private func setupData(){ - - } - - private func setupEvent(){ - - } -} - -class SettingCell: UITableViewCell { - var block: UIView! - var titleLabel: UILabel! - var arrowImageView: UIImageView! - - var line: LineView! - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupViews() { - backgroundColor = .clear - selectionStyle = .none - - block = { - let view = UIView() - view.backgroundColor = .c.csbn - view.layer.cornerRadius = 16 - view.layer.masksToBounds = true - contentView.addSubview(view) - view.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview()//.offset(12) - make.bottom.equalToSuperview()//.offset(-4) - make.height.equalTo(61) - } - return view - }() - - titleLabel = { - let label = UILabel() - label.font = .t.tts - label.textColor = .c.ctpn - block.addSubview(label) - label.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - } - return label - }() - - arrowImageView = { - let imageView = UIImageView() - imageView.image = MWIconFont.image(fromIcon: .arrowRightBorder, size: CGSizeMake(12, 12), color: .c.ctsn) - block.addSubview(imageView) - imageView.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - make.size.equalTo(CGSize(width: 12, height: 12)) - } - imageView.contentMode = .scaleAspectFit - return imageView - }() - - line = { - let v = LineView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview() - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - } - - func setupFirstCellCorner(){ - block.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - } - - func setupLastCellCorner(){ - block.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - } - - func setupFullCorner(){ - block.layer.maskedCorners = [ - .layerMinXMinYCorner, // 左上 - .layerMaxXMinYCorner, // 右上 - .layerMinXMaxYCorner, // 左下 - .layerMaxXMaxYCorner // 右下 - ] - } - - func setupEmptyCorner(){ - block.layer.maskedCorners = [] - } -} diff --git a/crush/Crush/Src/Modules/Me/View/MeHeaderView.swift b/crush/Crush/Src/Modules/Me/View/MeHeaderView.swift deleted file mode 100644 index 8657a41..0000000 --- a/crush/Crush/Src/Modules/Me/View/MeHeaderView.swift +++ /dev/null @@ -1,354 +0,0 @@ -// -// MeHeaderView.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit -import Combine -class MeHeaderView: UICollectionReusableView { - var avatarView: AvatarView! -// var avatarTopButton: UIButton! - // name row: - var namesStackH: UIStackView! - var nameLabel: UILabel! - var sexIcon: UIImageView! - // another row: - var idStackH: UIStackView! - var idLabel: UILabel! - var copyIcon: UIImageView! - var copyBtn: UIButton! - - // entrances row: - var entrancesBlock: UIView! - var entrancesStackH: UIStackView! - var entrance1: UIButton! - var entrance2: UIButton! - var entrance3: UIButton! - - var sectionTitle: UILabel! - var gradientBorderContainer: GradientBorderView! - var gradientLabel: ColorLabel! - var unlockVipButton:UIButton! - - var heightChangeBlock: ((CGFloat) -> Void)? - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - avatarView = { - let view = AvatarView() -// view.contentMode = .scaleAspectFill -// view.layer.cornerRadius = 40 -// view.layer.masksToBounds = true -// view.backgroundColor = .c.csbn - addSubview(view) - view.snp.makeConstraints { make in - make.width.height.equalTo(80) - make.top.equalToSuperview().offset(0) - make.centerX.equalToSuperview() - } - return view - }() - -// avatarTopButton = { -// let v = UIButton() -// addSubview(v) -// v.snp.makeConstraints { make in -// make.edges.equalTo(avatarView) -// } -// return v -// }() - - namesStackH = { - let view = UIStackView() - view.axis = .horizontal - view.spacing = 8 - view.alignment = .center - addSubview(view) - view.snp.makeConstraints { make in - make.top.equalTo(avatarView.snp.bottom).offset(8) - make.centerX.equalToSuperview() - make.leading.greaterThanOrEqualToSuperview().offset(CGFloat.lrs) - make.trailing.lessThanOrEqualToSuperview().offset(-CGFloat.lrs) - } - return view - }() - - nameLabel = { - let view = UILabel() - view.font = .t.thm - view.textColor = .c.ctpn - view.textAlignment = .left - view.numberOfLines = 1 - view.lineBreakMode = .byTruncatingTail - view.setContentHuggingPriority(.required, for: .horizontal) - view.setContentCompressionResistancePriority(.required, for: .horizontal) - namesStackH.addArrangedSubview(view) - return view - }() - - sexIcon = { - let view = UIImageView() - view.contentMode = .scaleAspectFit - view.image = UIImage(named: "sex_male_flag") - namesStackH.addArrangedSubview(view) - view.snp.makeConstraints { make in - make.width.height.equalTo(24) - } - return view - }() - - idStackH = { - let view = UIStackView() - view.axis = .horizontal - view.spacing = 8 - view.alignment = .center - addSubview(view) - view.snp.makeConstraints { make in - make.top.equalTo(namesStackH.snp.bottom).offset(8) - make.centerX.equalToSuperview() - make.leading.greaterThanOrEqualToSuperview().offset(CGFloat.lrs) - make.trailing.lessThanOrEqualToSuperview().offset(-CGFloat.lrs) - } - return view - }() - - idLabel = { - let view = UILabel() - view.font = .t.tbs - view.textColor = .c.ctsn - view.textAlignment = .left - view.numberOfLines = 1 - view.lineBreakMode = .byTruncatingTail - idStackH.addArrangedSubview(view) - return view - }() - - copyIcon = { - let view = UIImageView() - view.contentMode = .scaleAspectFit - view.image = MWIconFont.image(fromIcon: .copy, size: .init(width: 12, height: 12), color: .c.ctsn) - idStackH.addArrangedSubview(view) - return view - }() - - copyBtn = { - let view = UIButton() - view.setImage(UIImage(named: "copy_icon"), for: .normal) - view.addTarget(self, action: #selector(copyBtnAction), for: .touchUpInside) - addSubview(view) - view.snp.makeConstraints { make in - make.height.equalTo(24) - make.centerY.equalTo(idStackH) - make.leading.equalTo(idStackH) - make.trailing.equalTo(idStackH) - } - return view - }() - - entrancesBlock = { - let view = UIView() - view.backgroundColor = .c.csbn - view.layer.cornerRadius = 16 - view.layer.masksToBounds = true - addSubview(view) - view.snp.makeConstraints { make in - make.top.equalTo(idStackH.snp.bottom).offset(24) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return view - }() - - entrancesStackH = { - let view = UIStackView() - view.axis = .horizontal - view.spacing = 8 - view.alignment = .center - view.distribution = .fillEqually - entrancesBlock.addSubview(view) - view.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.bottom.equalToSuperview().offset(-16) - make.height.equalTo(68) - } - return view - }() - - entrance1 = { - let view = UIButton() - view.titleLabel?.font = UIFont.t.tls - view.setImage(UIImage(named: "me_header_membership"), for: .normal) - view.setTitle("Membership", for: .normal) - view.addTarget(self, action: #selector(entrance1Action), for: .touchUpInside) - entrancesStackH.addArrangedSubview(view) - - return view - }() - - entrance2 = { - let view = UIButton() - view.titleLabel?.font = UIFont.t.tls - view.setImage(UIImage(named: "me_header_diamonds"), for: .normal) - view.setTitle("Diamonds", for: .normal) - view.addTarget(self, action: #selector(entrance2Action), for: .touchUpInside) - entrancesStackH.addArrangedSubview(view) - return view - }() - - entrance3 = { - let view = UIButton() - view.titleLabel?.font = .t.tls - view.setImage(UIImage(named: "me_header_creator"), for: .normal) - view.setTitle("Creator", for: .normal) - view.addTarget(self, action: #selector(entrance3Action), for: .touchUpInside) - entrancesStackH.addArrangedSubview(view) - return view - }() - - sectionTitle = { - let view = UILabel() - view.font = .t.ttm - view.textColor = .c.ctpn - view.textAlignment = .left - view.numberOfLines = 1 - addSubview(view) - view.snp.makeConstraints { make in - make.top.equalTo(entrancesBlock.snp.bottom).offset(24) - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - // make.bottom.equalToSuperview().offset(-24) - } - return view - }() - - gradientBorderContainer = { - let gradient = CLSystemToken.gradient(token: .ccvn) - - let v = GradientBorderView(colors: gradient.colors(), gradientType: .leftToRight) - v.gBorderWidth = 1 - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - v.backgroundColor = .clear - addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(32) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.centerY.equalTo(sectionTitle) - } - return v - }() - - let vipItemsStackH = { - let v = UIStackView() - v.spacing = 4 - gradientBorderContainer.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - let tipIcon = { - let v = UIImageView() - v.image = UIImage(named: "vip_flag_16") - vipItemsStackH.addArrangedSubview(v) - return v - }() - tipIcon.isHidden = false - - gradientLabel = { - let v = ColorLabel() - v.applyGradient(.vip) - v.font = .t.tls - // v.textAlignment = .center - vipItemsStackH.addArrangedSubview(v) - return v - }() - - unlockVipButton = { - let v = UIButton() - v.addTarget(self, action: #selector(tapUnlockVIP), for: .touchUpInside) - gradientBorderContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - nameLabel.text = "nickname" - idLabel.text = "idxxxxx" - sectionTitle.text = "Characters 0/3" - gradientLabel.text = "Unlock More" - } - - private func setupData(){ - UserCore.shared.$user.sink {[weak self] user in - self?.nameLabel.text = user?.nickname - //self?.avatarView.loadImage(user?.headImage) - self?.avatarView.bindImageUrl(imgUrl: user?.headImage) - self?.sexIcon.image = user?.sex.icon - self?.idLabel.text = "ID: \(user?.idCard ?? "-")" - self?.setupRoleCount(valid: user?.createdAiCount ?? 0, total: user?.canCreateAiCount ?? 0) - - self?.gradientBorderContainer.isHidden = user?.isMember ?? false - }.store(in: &cancellables) - } - - public func setupRoleCount(valid:Int, total:Int){ - sectionTitle.text = "Characters \(valid)/\(total)" - } - - // MARK: - Action - @objc private func copyBtnAction() { - // copy idLabel's content to clipboard - if let idcard = UserCore.shared.user?.idCard, idcard.count > 0{ - UIPasteboard.general.string = idcard - Hud.toast(str: "Copy Successfully") - } - } - - @objc private func entrance1Action() { - AppRouter.goVIPCenter() - } - - @objc private func entrance2Action() { - AppRouter.goWalletCenter() - } - - @objc private func entrance3Action() { - AppRouter.goCreatorIntroduction() - } - - @objc private func tapUnlockVIP(){ - //AppRouter.goVIPCenter() - CLPurchase.shared.showVIPSubscribeSheet() - } - - override func layoutSubviews() { - super.layoutSubviews() - - entrance1.setUp(.top, padding: 8) - entrance2.setUp(.top, padding: 8) - entrance3.setUp(.top, padding: 8) - - let maxY = sectionTitle.frame.maxY + 24 - heightChangeBlock?(maxY) - } -} diff --git a/crush/Crush/Src/Modules/Me/View/MeRootPageView.swift b/crush/Crush/Src/Modules/Me/View/MeRootPageView.swift deleted file mode 100644 index 0f57b6b..0000000 --- a/crush/Crush/Src/Modules/Me/View/MeRootPageView.swift +++ /dev/null @@ -1,357 +0,0 @@ -// -// MeRootPageView.swift -// Crush -// -// Created by Leon on 2025/7/22. -// - -import UIKit - -class MeRootPageView: UIView { - var headerView: MeHeaderView! - - var layout: UICollectionViewFlowLayout! - var cv: UICollectionView! - - // Layout - var headerHeight: CGFloat = 300 - - // Data - var datas:[AIRoleInfo] = [AIRoleInfo]() - - var tapAvatarAction : (()-> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - cv = { - // item's height: w:h = 165:260, + 112 - let lr = CGFloat.lrs - let itemW = (UIScreen.width - lr * 2 - 16) * 0.5 - let itemH = itemW * 260 / 165.0 + 112 - - layout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = .init(top: 0, left: lr, bottom: 24 + UIWindow.safeAreaBottom, right: lr) - layout.itemSize = .init(width: itemW, height: itemH) - layout.headerReferenceSize = .init(width: UIScreen.width, height: headerHeight) - - let view = UICollectionView(frame: .zero, collectionViewLayout: layout) - view.backgroundColor = .clear //.c.cbd - view.delegate = self - view.dataSource = self - view.register(MeRootPageRollCell.self, forCellWithReuseIdentifier: "MeRootPageRollCell") - view.register(MeHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "MeHeaderView") - addSubview(view) - view.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.bottom.equalToSuperview() - } - - return view - }() - - } - - func config(datas:[AIRoleInfo]?){ - guard let roles = datas else{ - self.datas = [] - setupEmpty(empty: true) - return - } - - self.datas = roles - setupEmpty(empty: roles.count <= 0) - cv.reloadData() - } - - private func setupEmpty(empty: Bool){ - if(empty){ - showStartYEmpty(text: "No Character Yet", startY: 442 + UIWindow.statusBarHeight) - }else{ - removeEmpty() - } - } -} - -extension MeRootPageView: UICollectionViewDelegate, UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return self.datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MeRootPageRollCell", for: indexPath) as! MeRootPageRollCell - cell.cellType = .meRoleList - let data = datas[indexPath.item] - cell.config(data: data) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let data = datas[indexPath.item] - AppRouter.goAIRoleHome(aiId: data.aiId) - } - - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { - headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "MeHeaderView", for: indexPath) as? MeHeaderView - headerView.heightChangeBlock = { [weak self] height in - self?.headerHeight = height - // dlog("height change :\(height)") - self?.layout.headerReferenceSize = .init(width: UIScreen.width, height: height) - self?.layout.invalidateLayout() - } - headerView.avatarView.tapAction = {[weak self] in - self?.tapAvatarAction?() - } - return headerView - } - return UICollectionReusableView() - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - return .init(width: UIScreen.width, height: headerHeight) - } -} - -enum RoleGridCellType { - case meRoleList - case discoverList -} - -class MeRootPageRollCell: UICollectionViewCell { - var bgView: UIView! - - var gradientBorderView: GradientBorderView! - var avatarView: UIImageView! - - var overlayOnIv: GradientView! - var likeIconLabel: CLIconLabel! - var seeBanIv: UIImageView! - // --- - var nameLabel: UILabel! - var descLabel: UILabel! - - // Mobile tags - var tagsStackH: UIStackView! - - - var cellType: RoleGridCellType = .meRoleList - - // Datas - var datas: [AIRoleInfo]? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bgView = { - let v = UIView() - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - v.backgroundColor = .clear // .c.csbn - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(260.0 / 165.0) - make.top.equalToSuperview().offset(0) - } - return v - }() - - gradientBorderView = { - let gradient = CLSystemToken.gradient(token: .ccvn) - let color1 = gradient.firstColor! - let color2 = gradient.secondColor! - - let v = GradientBorderView(colors: [color1, color2], gradientType: .leftToRight) - v.gBorderWidth = 1 - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - v.backgroundColor = .c.csbn - bgView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - avatarView = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - v.backgroundColor = .random.withAlphaComponent(0.3) - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - v.clipsToBounds = true - bgView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1)) - } - return v - }() - - overlayOnIv = { - let colors = [UIColor.black.withAlphaComponent(0), UIColor.black] - let v = GradientView(colors: colors, gradientType: .topToBottom) - v.layer.cornerRadius = 16 - v.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - v.layer.masksToBounds = true - bgView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalTo(avatarView) - make.bottom.equalTo(avatarView) - make.height.equalTo(44) - } - return v - }() - - likeIconLabel = { - let v = CLIconLabel() - bgView.addSubview(v) - v.iconImageView.image = MWIconFont.image(fromIcon: .like, size: .init(width: 12, height: 12), color: .c.ctpn) - v.snp.makeConstraints { make in - make.height.equalTo(20) - make.leading.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-8) - } - return v - }() - - seeBanIv = { - let v = UIImageView() - bgView.addSubview(v) - v.backgroundColor = .c.csedn - v.layer.cornerRadius = 4 - v.layer.masksToBounds = true - v.contentMode = .center - v.image = MWIconFont.image(fromIcon: .eyeOff, size: CGSize(width: 12, height: 12), color: .c.ctpn) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 24, height: 24)) - make.top.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - - nameLabel = { - let v = UILabel() - v.font = .t.tts - v.textColor = .c.ctpn - v.textAlignment = .left - v.numberOfLines = 1 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(bgView.snp.bottom).offset(8) - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - } - return v - }() - - descLabel = { - let v = UILabel() - v.font = .t.tbs - v.textColor = .c.ctsn - v.textAlignment = .left - v.numberOfLines = 2 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(nameLabel.snp.bottom).offset(4) - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - } - return v - }() - - tagsStackH = { - let v = UIStackView() - v.axis = .horizontal - v.spacing = 8 - v.alignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(descLabel.snp.bottom).offset(8) - make.leading.equalToSuperview() - make.trailing.lessThanOrEqualToSuperview() - } - return v - }() - -// do { -// let tag = RoleTag() -// tag.title = "Sensibility" -// tag.style = .purple -// tagsStackH.addArrangedSubview(tag) -// } -// -// do { -// let tag = RoleTag() -// tag.title = "Romantic" -// tag.style = .theme -// tagsStackH.addArrangedSubview(tag) -// } - -// #warning("test data") -// testData() - } - - private func testData() { - nameLabel.text = "测试数据" - avatarView.image = UIImage(named: "eg") - descLabel.text = "desc desc desc desc desc desc desc desc" - likeIconLabel.contentLabel.text = "11.2k" - } - - public func config(data: AIRoleInfo?){ - guard let role = data else {return} - // 私密 - seeBanIv.isHidden = !(data?.permission == 2) - - nameLabel.text = role.nickname - descLabel.text = role.introduction ?? "" - avatarView.loadImage(role.homeImageUrl) - - let countDisplay = String.displayNumber(NSNumber(value: (data?.likedNum ?? 0)), scale: 1) - likeIconLabel.contentLabel.text = countDisplay - - tagsStackH.removeSubviews() - if let characterName = role.characterName{ - let tag = RoleTag() - tag.title = characterName -// if cellType == .meRoleList{ -// tag.style = .default -// }else{ -// tag.style = .blurPurple -// } - tag.style = .default - tagsStackH.addArrangedSubview(tag) - } - if let tagName = role.tagName{ - let tag = RoleTag() - tag.title = tagName -// if cellType == .meRoleList{ -// tag.style = .default -// }else{ -// tag.style = .blurTheme -// } - tag.style = .default - tagsStackH.addArrangedSubview(tag) - } - } -} diff --git a/crush/Crush/Src/Modules/Me/View/PersonInformationEditView.swift b/crush/Crush/Src/Modules/Me/View/PersonInformationEditView.swift deleted file mode 100644 index 25a9ec5..0000000 --- a/crush/Crush/Src/Modules/Me/View/PersonInformationEditView.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// PersonInformationEditView.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import UIKit -import DateToolsSwift -import Combine - -class PersonInformationEditView: UIView{ - lazy var birthdayPicker:BirthdayPickerView = BirthdayPickerView() - - var bottomButton: StyleButton! - - var container: LTScrollContainer! - var titleView: TitleView! - var nicknameTitleField: TitleTextField! - var sexView : CLSelectView! - var birthdaySelectView: CLSelectView! - - // Data - - @Published var nickname: String? - @Published var birthdayDate: Date? - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews(){ - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - nicknameTitleField = { - let v = TitleTextField() - v.maxLimit = 20 - v.minLimit = 2 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - sexView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - birthdaySelectView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - titleView.title = "login.personal.information".localized() - - nicknameTitleField.titleLabel.text = "nickname".localized() - nicknameTitleField.placeholder = "Nickname" - - sexView.titleLabel.text = "Sex" - sexView.showForceNormalSupportMsg("Gender can only modified once") - sexView.isEnabled = false - - birthdaySelectView.titleLabel.text = "Birthday" - birthdaySelectView.placeholder = "Birthday" - - bottomButton.setTitle("Submit", for: .normal) - - } - - private func setupData(){ - guard let user = UserCore.shared.user else{return} - - nickname = user.nickname - if let timestamp = user.birthday{ - birthdayDate = Date.dateFromMilliseconds(timestamp) - } - - self.sexView.contentStr = user.sex.localizedText - -// nicknameTitleField.textfield.text = user.nickname -// if let birthdayStr = user.birthday { -// let date = Date(dateString: birthdayStr, format: "yyyy-mm-dd") -// let formated = date.toString(dateFormat: "dd MMM, yyyy") -// birthdaySelectView.contentStr = formated -// } - } - - private func setupEvent(){ - birthdaySelectView.selectBlock = {[weak self] in - self?.selectBirthday() - } - - $nickname.sink { [weak self] str in - self?.nicknameTitleField.textfield.text = str - }.store(in: &cancellables) - - $birthdayDate.sink {[weak self] date in - guard let validDate = date else{return} - let years = Date().years(from:validDate) - if years < 18{ - self?.birthdaySelectView.showErrorMsg("Must be at least 18 years old.") - }else{ - self?.birthdaySelectView.hideErrorInfo() - } - let formated = validDate.toString(dateFormat: "dd MMM, yyyy") - self?.birthdaySelectView.contentStr = formated - }.store(in: &cancellables) - - nicknameTitleField.textfield.textPublisher.sink {[weak self] str in - self?.nickname = str?.trimmed - }.store(in: &cancellables) - } - - private func selectBirthday(){ - if let birthdayTimestamp = UserCore.shared.user?.birthday{ - let date = Date.dateFromMilliseconds(birthdayTimestamp) - birthdayPicker.setupDefaultSelectDate(date) - } - birthdayPicker.show() - - birthdayPicker.tapConfirmAction = {[weak self] date, timestamp in - dlog("birthday date:\(date). timestamp:\(timestamp)") - // self?.birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - self?.birthdayDate = date - } - } -} diff --git a/crush/Crush/Src/Modules/Role/Create/Base/RoleCreateBaseController.swift b/crush/Crush/Src/Modules/Role/Create/Base/RoleCreateBaseController.swift deleted file mode 100644 index 2abd877..0000000 --- a/crush/Crush/Src/Modules/Role/Create/Base/RoleCreateBaseController.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// RoleCreateBaseController.swift -// Crush -// -// Created by Leon on 2025/8/9. -// - -import UIKit - -class RoleCreateBaseController: CLViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - baseSetupViews() - baseSetupDats() - baseSetupEvents() - } - - private func baseSetupViews() { - navigationView.setupBackButtonCloseIcon() - navigationView.tapBackButtonAction = {[weak self] in - self?.alertToClose() - } - } - - private func baseSetupDats(){ - - } - - private func baseSetupEvents(){ - - } - - private func alertToClose(){ - view.endEditing(true) - - let alert = Alert(title: "内容未保存", text: "内容未保存,是否继续退出?") - let action1 = AlertAction(title: "退出", actionStyle: .destructive) {[weak self] in - self?.close(dismissFirst: true) - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - - - } - - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - -} diff --git a/crush/Crush/Src/Modules/Role/Create/Model/RoleCreateModels.swift b/crush/Crush/Src/Modules/Role/Create/Model/RoleCreateModels.swift deleted file mode 100644 index 2aed6ff..0000000 --- a/crush/Crush/Src/Modules/Role/Create/Model/RoleCreateModels.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// RoleCreateModels.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import Foundation - -struct AIUserRequest: Codable { - var aiId: Int? - var nickname: String? - var sex: Sex? - var headImg: String? - var birthday: String? - /// 角色Code - var roleCode: String? - /// 性格code - var characterCode: String? -// /// 用户输入的内容 -// var content: String? - var tagCode: String? - /// 10~300 - var introduction: String? - /// 1公开、2私密 - var permission: Int = 1 - /// 形象图 - var imageUrl: String? - /// 720x1280? - var imageWidth: Float = 720//String? - var imageHeight: Float = 1280 - var aiUserExt: AIUserRequestExt? - - /// 人物设定 用户输入 - var userProfile: String? - /// 对话风格 用户输入 - var userDialogueStyle: String? -} - -struct AIUserRequestExt: Codable { - /// 人物设定 10~2000 - var profile: String? - /// 对话风格 - var dialogueStyle: String? - /// 对话开场白 - var dialoguePrologue: String? - /// 对话音色Code - var dialogueTimbreCode: String? - /// 对话-音高 - var dialoguePitch: String? - /// 对话-语速 - var dialogueSpeechRate: String? - /// 对话音色url(可能是试听后的url,需要单独保存) - var dialogueTimbreUrl: String? - /// 形象风格code - var imageStyleCode: String? - /// 形象描述 - var imageDesc: String? - /// 形象参考。 可选 - var imageReferenceUrl: String? -} diff --git a/crush/Crush/Src/Modules/Role/Create/Model/RoleCreateViewModel.swift b/crush/Crush/Src/Modules/Role/Create/Model/RoleCreateViewModel.swift deleted file mode 100644 index 8bba57f..0000000 --- a/crush/Crush/Src/Modules/Role/Create/Model/RoleCreateViewModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// RoleCreateViewModel.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import Foundation - -class RoleCreateViewModel { - /// 请求构造体 - var requestParams: AIUserRequest // = AIUserRequest() - - /// 🔥编辑时此值存在 - var editAIInfo: AIUserModel? - - var settingAIGenerated:Bool = false - - @Published var batchNo: String = "" - - init() { - requestParams = AIUserRequest() - requestParams.aiUserExt = AIUserRequestExt() - } -} diff --git a/crush/Crush/Src/Modules/Role/Create/RoleCharacterSetController.swift b/crush/Crush/Src/Modules/Role/Create/RoleCharacterSetController.swift deleted file mode 100644 index 281d9fc..0000000 --- a/crush/Crush/Src/Modules/Role/Create/RoleCharacterSetController.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// RoleCharacterSetController.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit - -/// Step 2/4 -class RoleCharacterSetController: RoleCreateBaseController { - weak var viewModel: RoleCreateViewModel! - - lazy var styleSetController:RoleDialogStyleSetController = RoleDialogStyleSetController() - - override func viewDidLoad() { - super.viewDidLoad() - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - } - - private func setupDatas() { - - } - - private func restoreEditInfo(aiInfo: AIUserModel?) { - guard let info = aiInfo else { return } - - container.nickname = info.nickname ?? "" - container.restoreNoneEditableSex(sex: info.sex) - - if let birthday = info.birthday{ - let date = Date.dateFromMilliseconds(Int64(birthday)) - let birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - container.birthdayStr = birthdayStr//"2000-07-09" - } - container.settingContent = info.aiUserExt?.profile ?? "" - - } - - private func setupEvents() { - container.backButton.addTarget(self, action: #selector(tapBackButton), for: .touchUpInside) - container.bottomButton.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - container.generateButton.addTarget(self, action: #selector(tapGenerateButton), for: .touchUpInside) - - if viewModel.editAIInfo != nil{ - restoreEditInfo(aiInfo: viewModel.editAIInfo) - }else{ - // Set gender to Male by default when creating an AI role. - container.sexView.sex = .male - } - } - - @objc private func tapBottomButton() { - - guard container.nickname.count > 0 else { - dlog("nickname is empty") - return - } - Hud.showIndicator() - UserOperator.checkname(name: container.nickname, uid: viewModel.editAIInfo?.aiId) {[weak self] nicknameOK in - Hud.hideIndicator() - if(nicknameOK){ - self?.doGoNextStepStyleSet() - } - } - - } - - private func doGoNextStepStyleSet(){ - viewModel.requestParams.nickname = container.nickname - viewModel.requestParams.sex = container.sexSelect - viewModel.requestParams.birthday = container.birthdayStr - - let profile = container.settingTextView.textView.text.trimmed - viewModel.requestParams.aiUserExt?.profile = profile - viewModel.requestParams.userProfile = profile - - let vc = styleSetController - vc.viewModel = viewModel - navigationController?.pushViewController(vc, animated: true) - } - - @objc private func tapBackButton() { - close() - } - - @objc private func tapGenerateButton(){ - guard container.nickname.trimmed.count > 0 else{ - Hud.toast(str: "请输入昵称") - return - } - - if container.settingContent.count > 0{ - let alert = Alert(title: "内容覆盖", text: "AI创建的内容会覆盖你已经填写的内容,请确认是否继续?") - let action1 = AlertAction(title: "继续", actionStyle: .confirm) {[weak self] in - self?.doGenerateAISettingContent() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - }else{ - doGenerateAISettingContent() - } - } - - private func doGenerateAISettingContent(){ - Hud.showIndicator() - var params = self.viewModel.requestParams.toPartialDictionary(keys: ["characterCode", "tagCode", "introduction"]) - // var params = [String:Any]() - if container.settingContent.count > 0{ - params.updateValue(container.settingContent, forKey: "content") - } - - if container.nickname.count > 0{ - params.updateValue(container.nickname, forKey: "nickname") - } - if let sex = container.sexSelect{ - params.updateValue(sex.rawValue, forKey: "sex") - } - if let birthdayStr = container.birthdayStr{ - params.updateValue(birthdayStr, forKey: "birthday") - } - - params.updateValue("GEN_PROFILE_BY_NON", forKey: "ptType") - - AICowProvider.request(.aiContentGenerate(params: params), modelType: AIUserContentGenResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let content = success?.content{ - self?.container.settingAIGenerated = content - self?.viewModel.settingAIGenerated = true - } - case .failure: - return - } - } - } -} - diff --git a/crush/Crush/Src/Modules/Role/Create/RoleClassificationSelectController.swift b/crush/Crush/Src/Modules/Role/Create/RoleClassificationSelectController.swift deleted file mode 100644 index c086602..0000000 --- a/crush/Crush/Src/Modules/Role/Create/RoleClassificationSelectController.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// RoleClassificationSelectController.swift -// Crush -// -// Created by Leon on 2025/7/19. -// - -import UIKit - -/// Step 1 -class RoleClassificationSelectController: RoleCreateBaseController { - override class var shouldPresentThisVc: Bool { - return true - } - - lazy var viewModel: RoleCreateViewModel = RoleCreateViewModel() - - lazy var roleCharacterSetVc = RoleCharacterSetController() - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - } - - private func setupDats() { - - } - - private func restoreEditInfo(aiInfo: AIUserModel?) { - guard let info = aiInfo else { return } - - viewModel.requestParams.aiId = info.aiId - - container.restoreSelectRole(selectCode: info.roleCode) - container.restoreSelectPerson(selectCode: info.characterCode) - container.restoreSelectTag(selectCode: info.tagCode) - - } - - private func setupEvents() { - container.bottomButton.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - - restoreEditInfo(aiInfo: viewModel.editAIInfo) - } - - @objc private func tapBottomButton() { - viewModel.requestParams.tagCode = container.selectTagCode - viewModel.requestParams.roleCode = container.selectRoleCode - viewModel.requestParams.characterCode = container.selectCharacterCode - - let vc = roleCharacterSetVc - vc.viewModel = viewModel - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/crush/Crush/Src/Modules/Role/Create/RoleDialogStyleSetController.swift b/crush/Crush/Src/Modules/Role/Create/RoleDialogStyleSetController.swift deleted file mode 100644 index 6d4cc99..0000000 --- a/crush/Crush/Src/Modules/Role/Create/RoleDialogStyleSetController.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// RoleDialogStyleSetController.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit - -/// Step 3/4 -class RoleDialogStyleSetController: RoleCreateBaseController { - weak var viewModel: RoleCreateViewModel! - - lazy var rolePublish: RolePublishController = RolePublishController() - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - - } - - private func setupDats() { -// if let voice = AppDictManager.shared.aiDict?.timbreDictList?.first{ -// container.selectVoice = voice -// } - - - } - - private func restoreEditInfo(aiInfo: AIUserModel?) { - guard let info = aiInfo else { return } - - container.dialogStyleContent = info.aiUserExt?.dialogueStyle ?? "" - container.openingContent = info.aiUserExt?.dialoguePrologue ?? "" - - if let timbreDict = info.aiUserExt?.timbreDict{ - var voiceModel = VoiceModel(voiceDict: timbreDict) - if let dialoguePitch = info.aiUserExt?.dialoguePitch{ - voiceModel.dialoguePitch = dialoguePitch - } - if let dialogueSpeechRate = info.aiUserExt?.dialogueSpeechRate{ - voiceModel.dialogueSpeechRate = dialogueSpeechRate - } - container.selectVoice = voiceModel - - } - - } - - private func setupEvents() { - container.backButton.addTarget(self, action: #selector(tapBackButton), for: .touchUpInside) - container.bottomButton.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - container.generate1Button.addTarget(self, action: #selector(tapGenerate1Button), for: .touchUpInside) - container.generate2Button.addTarget(self, action: #selector(tapGenerate2Button), for: .touchUpInside) - container.vocieSelectView.selectBlock = {[weak self] in - self?.goVoiceSetup() - } - - restoreEditInfo(aiInfo: viewModel.editAIInfo) - } - - private func goVoiceSetup(){ - guard AppDictManager.shared.aiDict != nil else{ - Hud.showIndicator() - AppDictManager.shared.loadAIDict { _ in - Hud.hideIndicator() - } - return - } - - let vc = RoleVoiceSetController() - vc.viewModel = viewModel - vc.sexSelect = viewModel.requestParams.sex - vc.selectVoiceBefore = container.selectVoice - vc.selectVoiceAction = {[weak self] voice in - self?.container.selectVoice = voice - } - navigationController?.pushViewController(vc, animated: true) - } - - @objc private func tapBottomButton() { - let dialogueStyle = container.dialogStyleContent.trimmed - viewModel.requestParams.userDialogueStyle = dialogueStyle - viewModel.requestParams.aiUserExt?.dialogueStyle = dialogueStyle - viewModel.requestParams.aiUserExt?.dialoguePrologue = container.openingContent.trimmed - - guard let voice = container.selectVoice else { return } - viewModel.requestParams.aiUserExt?.dialogueTimbreCode = voice.voiceDict.code - viewModel.requestParams.aiUserExt?.dialoguePitch = voice.dialoguePitch - viewModel.requestParams.aiUserExt?.dialogueSpeechRate = voice.dialogueSpeechRate - viewModel.requestParams.aiUserExt?.dialogueTimbreUrl = voice.voiceDict.url - - let vc = rolePublish - vc.viewModel = viewModel - navigationController?.pushViewController(vc, animated: true) - } - - @objc private func tapBackButton() { - close() - } - - @objc private func tapGenerate1Button(){ - if container.dialogStyleContent.count > 0{ - let alert = Alert(title: "内容覆盖", text: "AI创建的内容会覆盖你已经填写的内容,请确认是否继续?") - let action1 = AlertAction(title: "继续", actionStyle: .confirm) {[weak self] in - self?.doGenerate1() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - }else{ - doGenerate1() - } - } - - private func doGenerate1(){ - var params = viewModel.requestParams.toPartialDictionary(keys: ["nickname", "sex", "birthday", "characterCode", "tagCode", "introduction"]) - if container.dialogStyleContent.count > 0{ - /// 根据用户输入进行生成开场白 -// params.updateValue("GEN_DIALOG_STYLE_BY_CONTENT", forKey: "ptType") - params.updateValue(container.dialogStyleContent, forKey: "content") - }else{ - /// 完全AI生成开场白 -// params.updateValue("GEN_DIALOG_STYLE_BY_NON", forKey: "ptType") - } - params.updateValue("GEN_DIALOG_STYLE_BY_NON", forKey: "ptType") - - Hud.showIndicator() - AICowProvider.request(.aiContentGenerate(params: params), modelType: AIUserContentGenResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let content = success?.content{ - self?.container.dialogStyleAIGenerated = content - } - case .failure: - return - } - } - } - - @objc private func tapGenerate2Button(){ - if container.openingContent.count > 0{ - let alert = Alert(title: "内容覆盖", text: "AI创建的内容会覆盖你已经填写的内容,请确认是否继续?") - let action1 = AlertAction(title: "继续", actionStyle: .confirm) {[weak self] in - self?.doGenerate2() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - }else{ - doGenerate2() - } - } - - private func doGenerate2(){ - var params = viewModel.requestParams.toPartialDictionary(keys: ["nickname", "sex", "birthday", "characterCode", "tagCode", "introduction"]) - //dlog("当前的prams[只生成部分]:\(params)") - - if container.openingContent.count > 0{ - /// 根据用户输入进行生成开场白 -// params.updateValue("GEN_PROLOGUE_BY_CONTENT", forKey: "ptType") - params.updateValue(container.openingContent, forKey: "content") - }else{ - /// 完全AI生成开场白 -// params.updateValue("GEN_PROLOGUE_BY_NON", forKey: "ptType") - } - params.updateValue("GEN_PROLOGUE_BY_NON", forKey: "ptType") - - Hud.showIndicator() - AICowProvider.request(.aiContentGenerate(params: params), modelType: AIUserContentGenResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let content = success?.content{ - self?.container.openingAIGenerated = content - } - case .failure: - return - } - } - } -} - diff --git a/crush/Crush/Src/Modules/Role/Create/RoleFigureGenerateController.swift b/crush/Crush/Src/Modules/Role/Create/RoleFigureGenerateController.swift deleted file mode 100644 index fe3a610..0000000 --- a/crush/Crush/Src/Modules/Role/Create/RoleFigureGenerateController.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// RoleFigureGenerateController.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import UIKit -import TZImagePickerController -import Combine - -/// Step 4/4 首次生成封面专用,和编辑图片生成流程不一致 -class RoleFigureGenerateController: CLViewController { - - weak var viewModel: RoleCreateViewModel! - - var generatedBatchNoAction: ((String, String, String, String?) -> Void)? - - // Data - @Published var referenceImage:UIImage? - private var subscriptions = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[weak self] in - self?.container.scrollSelectedShow() - } - } - - - private func setupViews() { - } - - private func setupDats() { - } - - private func setupEvents() { - container.bottomButton.addTarget(self, action: #selector(bottomButtonTapped), for: .touchUpInside) - container.referenceButton.addTarget(self, action: #selector(referenceButtonTapped), for: .touchUpInside) - container.generate1Button.addTarget(self, action: #selector(tapAIGenerated), for: .touchUpInside) - - container.referenceButton.closeAction = {[weak self] _ in - self?.referenceImage = nil - } - - $referenceImage.sink {[weak self] image in - self?.container.referenceButton.bindSuccessImage(image) - }.store(in: &subscriptions) - - if let code = viewModel.requestParams.aiUserExt?.imageStyleCode, let desc = viewModel.requestParams.aiUserExt?.imageDesc{ - container.figureDescriptionContent = desc - - container.selectStyleId = code - }else if viewModel.editAIInfo != nil{ - // 编辑模式恢复 - restoreEditInfo(aiInfo: viewModel.editAIInfo) - } - } - - private func restoreEditInfo(aiInfo: AIUserModel?) { - guard let info = aiInfo else { return } - - container.figureDescriptionContent = info.aiUserExt?.imageDesc ?? "" - - container.selectStyleId = info.aiUserExt?.imageStyleCode ?? "" - container.selectStylePrompt = info.aiUserExt?.imageStyleDict?.prompt ?? "" - } - - // MARK: - Action - @objc private func bottomButtonTapped() { - print("bottomButtonTapped") - var params = viewModel.requestParams.toPartialDictionary(keys: ["sex", "birthday"]) //[String:Any]() - - // imageStylePrompt 形象风格提示词(预设的几种) - let imageStylePrompt = container.selectStylePrompt - let imageStyleCode = container.selectStyleId - params.updateValue(imageStylePrompt, forKey: "imageStylePrompt") - viewModel.requestParams.aiUserExt?.imageStyleCode = imageStyleCode - - // content:用户形象描述 or 生成的 - let description = container.descriptionTextView.textView.text.trimmed - params.updateValue(description, forKey: "content") - viewModel.requestParams.aiUserExt?.imageDesc = description - - // imageReferenceUrl, 形象参考base64 - var referenceBase64 = "" - if let img = referenceImage { - referenceBase64 = img.jpegBase64String(compressionQuality: 0.8)! - params.updateValue(referenceBase64, forKey: "imageReferenceUrl") - } - - if viewModel.editAIInfo != nil{ - params.updateValue(false, forKey: "hl") - } - - params.updateValue(AIImageGenerateType.CREATE_AI_IMAGE.rawValue, forKey: "type") - - Hud.showIndicator() - AICowProvider.request(.imageGenerateCreateTask(params: params), modelType: AIUserImageTaskCreateResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let response = success, let batchNo = response.batchNo{ - self?.generatedBatchNoAction?(batchNo, imageStyleCode, description, referenceBase64) - self?.viewModel.batchNo = batchNo - self?.navigationController?.popToViewControllerType(type: RolePublishController.self) - } - case .failure(let failure): - dlog(failure) - } - } - } - - @objc private func tapAIGenerated(){ - if container.figureDescriptionContent.count > 0{ - let alert = Alert(title: "内容覆盖", text: "AI创建的内容会覆盖你已经填写的内容,请确认是否继续?") - let action1 = AlertAction(title: "继续", actionStyle: .confirm) {[weak self] in - self?.doGenerateAlbumDesc() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - }else{ - doGenerateAlbumDesc() - } - } - - private func doGenerateAlbumDesc(){ - var params = viewModel.requestParams.toPartialDictionary(keys: ["nickname", "sex", "birthday", "characterCode", "tagCode", "introduction"]) - /// 形象描述 - // 新需求: 总是覆盖 - params.updateValue("GEN_AI_IMAGE_DESC_BY_NON", forKey: "ptType") - if container.figureDescriptionContent.count > 0{ - params.updateValue(container.figureDescriptionContent, forKey: "content") - } - - Hud.showIndicator() - AICowProvider.request(.aiContentGenerate(params: params), modelType: AIUserContentGenResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let content = success?.content{ - self?.container.figureDescriptionAIGenerated = content - } - case .failure: - return - } - } - } - - @objc private func referenceButtonTapped() { - guard let picker = ImagePicker(maxImagesCount: 1, delegate: self) else { return } - picker.allowPickingGif = false -// picker.allowCrop = true -// picker.allowPreview = true - picker.showSelectBtn = false - picker.needCircleCrop = true - - picker.cropRect = CGRect(origin: .init(x: 0, y: 0), size: CGSize(width: UIScreen.width, height: UIScreen.height)) - picker.circleCropRadius = Int((UIScreen.width - 37*2) * 0.5) - present(picker, animated: true, completion: nil) - } -} - -extension RoleFigureGenerateController:TZImagePickerControllerDelegate{ - - func imagePickerController(_: TZImagePickerController!, - didFinishPickingPhotos photos: [UIImage]!, - sourceAssets _: [Any]!, - isSelectOriginalPhoto _: Bool, - infos _: [[AnyHashable: Any]]!) - { - guard let img = photos.first else { - return - } - referenceImage = img - } - - -} diff --git a/crush/Crush/Src/Modules/Role/Create/RolePublishController.swift b/crush/Crush/Src/Modules/Role/Create/RolePublishController.swift deleted file mode 100644 index 60c0fa2..0000000 --- a/crush/Crush/Src/Modules/Role/Create/RolePublishController.swift +++ /dev/null @@ -1,406 +0,0 @@ -// -// RolePublishController.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit -import Combine -import TZImagePickerController - -/// 生成角色页面 -/// 选参考图 -class RolePublishController: RoleCreateBaseController { - weak var viewModel: RoleCreateViewModel! - -// @Published var image: UIImage? - private var cancellables = Set() - - var isAllImagesOK: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - disabledFullScreenPan() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - enabledFullScreenPan() - } - - private func setupViews() { - - container.container.scrollView.delegate = self - } - - private func setupDatas(){ -// if let editAIInfo = viewModel.editAIInfo, let url = editAIInfo.imageUrl{ -// var usingImage = AIUserImageQuery() -// usingImage.imageUrl = url -// usingImage.status = .completed -// container.appearanceResult.usingFigureImage = usingImage -// } - if let editAIInfo = viewModel.editAIInfo{ - viewModel.requestParams.aiUserExt?.imageDesc = editAIInfo.aiUserExt?.imageDesc - viewModel.requestParams.aiUserExt?.imageStyleCode = editAIInfo.aiUserExt?.imageStyleCode - } - - // -- 文案 - var title = "Create" - var bottomButtonTitle = "Create" - if viewModel.editAIInfo != nil { - title = "Modify" - bottomButtonTitle = "Modify" - } - navigationView.alpha0Title = title - container.titleView.title = title - container.bottomButton.setTitle(bottomButtonTitle, for: .normal) - } - - private func setupEvents(){ - container.backButton.addTarget(self, action: #selector(tapBackButton), for: .touchUpInside) - container.bottomButton.addTarget(self, action: #selector(bottomButtonTapped), for: .touchUpInside) - container.generate1Button.addTarget(self, action: #selector(tapAIGeneratedIntroductionButton), for: .touchUpInside) - container.appearanceResult.generateButton.addTarget(self, action: #selector(tapRegenerateFigureButton), for: .touchUpInside) - container.uploadImageView.bgTapBlock = {[weak self] in - //self?.pickAppearance() - self?.goCreateAIGeneratePics() - } - - viewModel.$batchNo.sink {[weak self] result in - self?.container.showQueryGeneratedImages(generating: !result.isEmpty) - self?.loadingGeneratingResults(batchNo: result) - }.store(in: &cancellables) - - container.$introductionContent.sink {[weak self] string in - self?.viewModel.requestParams.introduction = string - }.store(in: &cancellables) - - if viewModel.editAIInfo == nil{ - doGenerate() - }else{ - restoreEditInfo(aiInfo: viewModel.editAIInfo) - } - } - - private func restoreEditInfo(aiInfo: AIUserModel?) { - guard let info = aiInfo else { return } - - container.privacyPublic = (info.permission ?? 2) == 1 - container.introductionContent = info.introduction ?? "" - - container.showQueryGeneratedImages(generating: true) - - // 图片 - let last = AIUserImageQuery() - last.imageUrl = info.imageUrl - last.status = .completed - container.appearanceResult.usingFigureImage = last - container.appearanceResult.selectImageUrl = info.imageUrl ?? "" - - //头像 - let photo = UploadPhotoM() - photo.setupValidImageUrl(url: info.headImg) - container.avatarModel = photo - } - - // MARK: - Functions - - func loadingGeneratingResults(batchNo : String?){ - if isAllImagesOK { - return - } - - guard let queryNo = batchNo, queryNo.isEmpty == false else{ - dlog("❌batchNo is nil") - return - } - - container.appearanceResult.setupLastGenerateProcessStart() - - dlog("📖查询图片生成中...") - AICowProvider.request(.imageGeneratedQuery(batchNo: queryNo), modelType: [AIUserImageQuery].self) { [weak self] result in - switch result { - case .success(let success): - guard let queryDatas = success else{ - return - } - self?.container.appearanceResult.config(success) - var ok = true - if(queryDatas.count < 6){ - ok = false - }else{ - for per in queryDatas { - if per.status == .pending{ - ok = false - } - } - } - self?.isAllImagesOK = ok - if ok == false{ - DispatchQueue.main.asyncAfter(deadline: .now() + 6) {[weak self] in - self?.loadingGeneratingResults(batchNo: self?.viewModel.batchNo) - } - }else{ - // ✅ - self?.container.appearanceResult.setupLastGenerateProcessEnd() - } - case .failure: - break - } - } - - } - - - /// 创建时免费生成图像 - private func goCreateAIGeneratePics(){ - - let vc = RoleFigureGenerateController() - vc.viewModel = viewModel - navigationController?.pushViewController(vc, animated: true) - - vc.generatedBatchNoAction = {[weak self] batchNo, imageStyleCode,description, referenceBase64 in - self?.viewModel.requestParams.aiUserExt?.imageStyleCode = imageStyleCode - self?.viewModel.requestParams.aiUserExt?.imageDesc = description - - // 新图片 - if batchNo.count > 0{ - self?.isAllImagesOK = false - self?.container.appearanceResult.config([], reset: true) // 先loading着 - self?.container.showQueryGeneratedImages(generating: true) - self?.loadingGeneratingResults(batchNo: batchNo) - } - } - } - - /// 编辑额外生成图片 - private func goEditAIRegeneratePics(){ - let vc = RolePhotoGenerateController(type: .figure) - vc.aiId = viewModel.requestParams.aiId - vc.introduction = container.introductionContent - vc.sex = viewModel.requestParams.sex - vc.birthdayString = viewModel.requestParams.birthday - vc.preSelectstyleCode = viewModel.requestParams.aiUserExt?.imageStyleCode // "IS0020"// - vc.prePhotoDesc = viewModel.requestParams.aiUserExt?.imageDesc - presentNaviRootVc(vc: vc) - - vc.confirmSelectAction = {[weak self] imageStyleCode, desc, imgs in - self?.viewModel.requestParams.aiUserExt?.imageStyleCode = imageStyleCode - self?.viewModel.requestParams.aiUserExt?.imageDesc = desc - - guard let image = imgs?.first else {return} - self?.container.loadDataAndSelect(image) - } - } - - private func judgeAlertToPublishNewAIRole(){ -// Alert.showAIRoleCreateSuccessAlert { -// Hud.hideIndicator() -// } confirmAction: {[weak self] in -// self?.doPublishRole() -// } - - if viewModel.editAIInfo != nil{ - // 编辑AI - doPublishRole() - }else{ - // 创建AI Role - if UserCore.shared.user?.canCreateAIRole() ?? false{ - // 能创建 - Alert.showAIRoleCreateSuccessAlert { - Hud.hideIndicator() - } confirmAction: {[weak self] in - self?.doPublishRole() - } - } else { - if UserCore.shared.user?.isMember == false{ // 去订VIP - Hud.hideIndicator() - let sheet = VIPSubscribeSheet() - sheet.show() - }else { -// #warning("是否主动Alert 到底创建限制") - Alert.showAIRoleCreateSuccessAlert { - Hud.hideIndicator() - } confirmAction: {[weak self] in - self?.doPublishRole() - } - } - } - - } - } - - private func doPublishRole(){ -//#warning("test") -// guard let aiId = viewModel.requestParams.aiId else{ -// return -// } -// AppRouter.goBackRootController(jumpIndex: .me) -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { -// AppRouter.goChatVC(aiId: aiId) -// AppRouter.alertCreateAlbumsToRoleHome(aiId: aiId) -// } -// view.hideToastActivity() -// return - -// close(dismissFirst: true) -// AppRouter.goBackRootController(jumpIndex: .me) -// close(dismissFirst: true) { -// let vc = UIWindow.getTopViewController() -// if let current = UIWindow.getTopViewController(), current.isKind(of: RoleHomePagerController.self){ -// // Stay in RoleHomePagerController -// }else{ -// AppRouter.goBackRootController(jumpIndex: .me) -// } -// dlog("🔥current vc: \(String(describing: vc))") -// } -// return - - guard let avatar = container.avatarModel?.remoteFullPath else { - dlog("invalid ai role avatar") - return - } - - viewModel.requestParams.headImg = avatar - - viewModel.requestParams.introduction = container.introductionContent - viewModel.requestParams.permission = container.privacyPublic ? 1 : 2 - var params = viewModel.requestParams.toNonNilDictionary() - - let editAIInfo = viewModel.editAIInfo - - params.updateValue(container.appearanceResult.selectImageUrl, forKey: "imageUrl") - - //dlog("🔥当前params: \(params)") - //Hud.showIndicator() - AIRoleProvider.request(.aiUserCreateEdit(params: params), modelType: AICreateResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case let .success(success): - NotificationCenter.post(name: .aiRoleCreatedOrDelete) - NotificationCenter.post(name: .aiRoleInfoChanged) - if editAIInfo != nil{ - dlog("编辑AI 退出") - // 编辑保存成功 - self?.close(dismissFirst: true) { - // _ = UIWindow.getTopViewController() - if let current = UIWindow.getTopViewController(), current.isKind(of: RoleHomePagerController.self){ - // Stay in RoleHomePagerController - }else{ - AppRouter.goBackRootController(jumpIndex: .me) - } - } - }else{ - // 新建用户 - dlog("新建AI 退出") - AppRouter.goBackRootController(jumpIndex: .me) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - AppRouter.goChatVC(aiId: success?.aiId) - AppRouter.alertCreateAlbumsToRoleHome(aiId: success?.aiId) - } - } - - case .failure: - break - } - } - } - - @objc private func tapAIGeneratedIntroductionButton(){ - if container.introductionContent.count > 0{ - let alert = Alert(title: "内容覆盖", text: "AI创建的内容会覆盖你已经填写的内容,请确认是否继续?") - let action1 = AlertAction(title: "继续", actionStyle: .confirm) {[weak self] in - self?.doGenerate() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - }else{ - doGenerate() - } - } - - private func doGenerate(){ - var params = viewModel.requestParams.toPartialDictionary(keys: ["nickname", "sex", "birthday", "characterCode", "tagCode", "introduction"]) - /// 形象描述 - params.updateValue("GEN_INTRODUCTION", forKey: "ptType") - - if container.introductionContent.count > 0{ - params.updateValue(container.introductionContent, forKey: "content") - } - - if let dialogue = viewModel.requestParams.aiUserExt?.dialogueStyle{ - params.updateValue(dialogue, forKey: "dialogue") - } - - if let figure = viewModel.requestParams.aiUserExt?.profile{ - params.updateValue(figure, forKey: "figure") - } - - Hud.showIndicator() - AICowProvider.request(.aiContentGenerate(params: params), modelType: AIUserContentGenResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let content = success?.content{ - self?.container.contentAIGenerated = content - } - case .failure: - return - } - } - } - - @objc private func tapRegenerateFigureButton(){ - // 尝试重新生成图片 - if viewModel.requestParams.aiId != nil{ - goEditAIRegeneratePics() - }else{ - goCreateAIGeneratePics() - } - } - - @objc private func bottomButtonTapped(){ - // 头像是否已上传? - if container.avatarModel?.remoteFullPath != nil{ - Hud.showIndicator() - judgeAlertToPublishNewAIRole() - }else{ - // 头像上传... - guard let photo = container.avatarModel else { return } - Hud.showIndicator() - CloudStorage.shared.s3BatchAddPhotos([photo], bucket: .ROLE) {[weak self] result in - if result{ - //self?.doPublishRole() - self?.judgeAlertToPublishNewAIRole() - }else{ - Hud.hideIndicator() - } - } - } - } - - @objc private func tapBackButton() { - close() - } -} - - -extension RolePublishController: UIScrollViewDelegate{ - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } - -} diff --git a/crush/Crush/Src/Modules/Role/Create/View/PhotoGenerateStateOverlay.swift b/crush/Crush/Src/Modules/Role/Create/View/PhotoGenerateStateOverlay.swift deleted file mode 100644 index ff52c70..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/PhotoGenerateStateOverlay.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// PhotoGenerateStateOverlay.swift -// Crush -// -// Created by Leon on 2025/9/24. -// - -import APNGKit -class PhotoGenerateStateOverlay: UIView { - var loadingStackV: UIStackView! - var loadingIndicator: APNGImageView! - var loadingLabel: UILabel! - - var failedStackV: UIStackView! - var failedIcon: UIImageView! - var failedLabel: UILabel! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.cbd - cornerRadius = 16 - - setupLoadingViews() - setupFailedViews() - } - - override func layoutSubviews() { - super.layoutSubviews() - addDashedBorder(to: self, cornerRadius: 16) - } - - private func setupLoadingViews() { - loadingStackV = { - let v = UIStackView() - v.alignment = .center - v.axis = .vertical - v.spacing = 8 - addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - loadingIndicator = { - let v = APNGImageView() - - v.contentMode = .scaleAspectFit - loadingStackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - // make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 96, height: 96)) - } - - if let url = Bundle.main.url(forResource: "generating", withExtension: "png") { - let image = try? APNGImage(fileURL: url) - v.image = image - } - v.stopAnimating() - return v - }() - - loadingLabel = { - let v = UILabel() - v.textColor = .white - v.font = .t.tls - v.textAlignment = .center - loadingStackV.addArrangedSubview(v) - v.text = "Generating..." - return v - }() - - loadingStackV.isHidden = true - } - - private func setupFailedViews() { - failedStackV = { - let v = UIStackView() - v.alignment = .center - v.axis = .vertical - v.spacing = 16 - addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - } - return v - }() - - failedIcon = { - let v = UIImageView() - v.image = UIImage(named: "status-error") - failedStackV.addArrangedSubview(v) - return v - }() - - failedLabel = { - let v = UILabel() - v.textColor = .white - v.font = .t.tls - v.textAlignment = .center - failedStackV.addArrangedSubview(v) - v.text = "Generate Failed" - return v - }() - - failedStackV.isHidden = true - } - - func showLoading() { - isHidden = false - loadingStackV.isHidden = false - failedStackV.isHidden = true - - loadingIndicator.startAnimating() - } - - func hideLoading() { - loadingStackV.isHidden = true - loadingIndicator.stopAnimating() - } - - func showFailed() { - isHidden = false - hideLoading() - - loadingStackV.isHidden = true - failedStackV.isHidden = false - } -} diff --git a/crush/Crush/Src/Modules/Role/Create/View/RoleCharacterSetView.swift b/crush/Crush/Src/Modules/Role/Create/View/RoleCharacterSetView.swift deleted file mode 100644 index 0c8e4c2..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/RoleCharacterSetView.swift +++ /dev/null @@ -1,299 +0,0 @@ -// -// RoleCharacterSetView.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import UIKit -import Combine - -class RoleCharacterSetView: UIView { - lazy var birthdayPicker:BirthdayPickerView = BirthdayPickerView() - - var backButton: EPIconTertiaryButton! - var bottomButton: StyleButton! - var container: LTScrollContainer! - var titleView: TitleView! - var stepLabel: UILabel! - - var nicknameTitleView: TitleTextField! - var sexView: GenderSelectView! - /// 编辑时展示用,不可操作 - var sexSelectView : CLSelectView! - var birthdaySelectView: CLSelectView! - var settingTextView: TitleTextView! - var generateButton: TextButton! - - // Data - @Published var nickname: String = "" - /// 用户输入 - @Published var sexSelect: Sex? - - @Published var birthdayStr: String? - @Published var birthdayDate: Date? - - @Published var settingContent: String = "" - @Published var settingAIGenerated: String = "" - - @Published var canGoNext : Bool = false - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupData(){ - - } - - private func setupEvent(){ - - - sexView.$sex.sink {[weak self] sex in - self?.sexSelect = sex - dlog("Sex: \(String(describing: sex))") - }.store(in: &cancellables) - - $settingAIGenerated.sink {[weak self] str in -// self?.settingTextView.textView.text = str - self?.settingContent = str - }.store(in: &cancellables) - - $birthdayStr.sink {[weak self] birdayStr in - self?.birthdaySelectView.contentStr = birdayStr - }.store(in: &cancellables) - - $nickname.sink {[weak self] str in - self?.nicknameTitleView.textfield.text = str - }.store(in: &cancellables) - - nicknameTitleView.textfield.textPublisher.sink {[weak self] str in - self?.nickname = str ?? "" - }.store(in: &cancellables) - - // Option 2 -// nicknameTitleView.textfield.textDidChanged = {[weak self] textfield in -// self?.nickname = textfield.text?.trimmed -// } - - $settingContent.sink {[weak self] str in - self?.settingTextView.defaultText = str - }.store(in: &cancellables) - - Publishers.CombineLatest4($nickname, $sexSelect, $birthdayStr, $settingContent).map{ - nickname, sex, birthdayStr, text in - if nickname.isValidNickname && sex != nil && (birthdayStr ?? "").count > 0 && text.trimmed.count >= 10{ - return true - } - return false - }.assign(to: &$canGoNext) - - $canGoNext.sink {[weak self] can in - self?.bottomButton.isEnabled = can - }.store(in: &cancellables) - - birthdaySelectView.selectBlock = {[weak self] in - self?.selectBirthday() - } - - settingTextView.textDidChanged = {[weak self] textfield in - self?.settingContent = textfield.text - } - - #if DEBUG - testData() - #endif - - // 创建AI,默认生日 - birthdayStr = "2000-01-01" - } - - func restoreNoneEditableSex(sex: Sex?){ - self.sexSelectView.contentStr = (sex ?? .noncomfirming).localizedText - self.sexSelectView.isHidden = false - - self.sexView.isHidden = true; - self.sexView.sex = sex - } - - private func testData(){ - nickname = "Monab" - //sexView.sex = .female - birthdayStr = "2000-07-09" - settingContent = "在某一个平凡的清晨阳光透过薄薄的窗帘洒落在房间里空气中漂浮着淡淡的咖啡香气时钟滴答作响仿佛在提醒着新的一天已经开始世界在悄然运转而人们也在各自的节奏里迎接生活有人匆忙穿过街道去赶上地铁有人推开门迎接清风与日光有人依旧在梦中与自己短暂的安宁相伴而这一切都构成了城市每日的序曲生活的美好并不总是轰轰烈烈它常常隐藏在最琐碎的细节里比如早晨一杯热牛奶的温度比如路口遇见陌生人礼貌的点头比如远方的朋友忽然发来的一句问候让你觉得再孤单的日子也有温暖的痕迹我们习惯把目光投向远方幻想着某一天能抵达一个理想的未来却常常忘了眼前的每一分每一秒才是真正的存在当我们追逐着尚未抵达的目标心里常常会生出焦虑觉得自己不够快不够好不够幸运但如果静下心来仔细看看你会发现其实生命从来没有亏待过任何人它总会在不同的阶段以不同的方式让你体验成长与收获也许是一段艰难的经历让你学会坚强也许是一场意外的遇见让你感到世界柔软也许是一份迟来的鼓励让你重新点燃希望人生从不是一条直线而更像是一条蜿蜒的河流它绕过高山经过峡谷有时奔腾有时平缓而我们要做的不是预测下一个转弯会带来什么而是学会在当下的河水里尽情漂流感受每一段风景带来的独特意义很多时候我们被告知要努力要成功要拥有别人眼中的光鲜亮丽于是拼命向前不敢停歇生怕被落下生怕被否定可真正值得珍惜的却往往是那些慢下来的时刻是与家人坐在一起谈笑风生的晚餐是一个人在书桌前静静读完一本书的午后是走在公园小径上不经意抬头看见的那片湛蓝天空这些微小而真实的片段才是支撑我们走下去的力量幸福不是宏大的目标而是一点一滴的积累正如滴水汇聚成河尘埃凝聚成星无数平凡的瞬间堆叠出生命的厚度很多人说他们害怕孤独害怕失败害怕努力没有回报可是如果换一个角度想想孤独是一种与自己对话的机会失败是一次重塑的契机努力没有回报的过程里也藏着你独有的成长轨迹当我们真正理解这些或许就会少一些抱怨多一些笃定少一些焦虑多一些耐心生命的意义从来不是一个标准答案它更像是一场没有剧本的旅行你无法预知下一个站点会遇见什么也无法控制沿途的天气是否晴朗你能做的只是带着一颗开放的心勇敢走下去接受遇见的一切并在其中找到属于自己的答案而当你慢慢这样生活时你会惊讶地发现世界似乎也在悄然回应你给你更多的惊喜和美好我们每个人都在与时间赛跑却也都在与自己和解从青春的轻狂到中年的稳重再到老年的淡然每一个阶段都有它独特的价值与美丽没有什么是完全的遗憾也没有什么是彻底的圆满人生的意义就在于这种不完美中的坚持与探索当我们终于有一天回头去看那些走过的路会发现原来所谓的结果从来没有那么重要真正让人感动的始终是那些一路陪伴的风景和一路成长的自己所以请不要过于焦虑未来也不要过度执着过去珍惜当下的每一个笑容每一次拥抱每一份努力因为它们才是你生命中最真实的财富人生就像是一首没有句号的诗长短不一起承转合我们无法决定开头和结尾却能决定如何书写中间的篇章而这篇章也终将汇成属于你自己的故事无论是精彩还是平凡都是独一无二的存在当你明白这一点你就会更加从容更加勇敢地走向前方" - } - - // MARK: - Action - private func selectBirthday(){ - if let date = birthdayDate{ - birthdayPicker.setupDefaultSelectDate(date) - }else if let dateStr = birthdayStr{ - let date = Date(dateString: dateStr, format: "yyyy-MM-dd") - birthdayPicker.setupDefaultSelectDate(date) - } - - birthdayPicker.show() - birthdayPicker.tapConfirmAction = {[weak self] date, timestamp in - // dlog("birthday date:\(date). timestamp:\(timestamp)") - self?.birthdayDate = date - self?.birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - } - } - - private func setupViews() { - backButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .large, iconCode: .arrowLeftBorder) - let size = v.bgImageSize() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - make.size.equalTo(CGSize(width: 88, height: size.height)) - } - return v - }() - - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - // make.leading.equalToSuperview().offset(CGFloat.lrs) - make.leading.equalTo(backButton.snp.trailing).offset(16) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - stepLabel = { - let v = UILabel() - v.font = CLSystemToken.font(token: .tlm) - v.textColor = .c.ctsn - v.text = "2/4" - titleView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(titleView.titleLabel) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - nicknameTitleView = { - let v = TitleTextField() - container.stack.addArrangedSubview(v) - // 2-20 - v.minLimit = 2 - v.maxLimit = 20 - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - sexView = { - let v = GenderSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - sexSelectView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - v.isHidden = true - return v - }() - - birthdaySelectView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - settingTextView = { - let v = TitleTextView() - /// 10~2000 - v.maxLimit = 4000 - v.minLimit = 10 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - generateButton = { - let v = TextButton() - v.text = "自动生成" - settingTextView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview()//.offset(-CGFloat.lrs) - make.centerY.equalTo(settingTextView.titleLabel) - make.height.equalTo(40) - } - return v - }() - - titleView.title = "Character" - nicknameTitleView.titleLabel.text = "Nickname" - nicknameTitleView.placeholder = "对角色的称呼" - nicknameTitleView.errorMsg = "昵称只能包含2-20字符" - - sexSelectView.titleLabel.text = "Sex" - sexSelectView.showForceNormalSupportMsg("Gender can only modified once") - sexSelectView.isEnabled = false - - birthdaySelectView.titleLabel.text = "Birthday" - birthdaySelectView.placeholder = "请选择角色生日" - bottomButton.setTitle("Next", for: .normal) - settingTextView.titleLabel.text = "Setting" - settingTextView.placeholder = "请描述角色的背景、性格、身份" - settingTextView.errorMsg = "设定只能包含10-4000个字符" - } -} diff --git a/crush/Crush/Src/Modules/Role/Create/View/RoleClassificationSelect.swift b/crush/Crush/Src/Modules/Role/Create/View/RoleClassificationSelect.swift deleted file mode 100644 index 2472aea..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/RoleClassificationSelect.swift +++ /dev/null @@ -1,555 +0,0 @@ -// -// RoleClassificationSelect.swift -// Crush -// -// Created by Leon on 2025/7/21. -// -import UIKit - -class RoleClassificationSelect: UIView { - var bottomButton: StyleButton! - var container: LTScrollContainer! - var titleView: TitleView! - var stepLabel: UILabel! - - var roleContainer: UIView! - var roleFirstSectionFlowLayout: FlowAutoLayoutContainer! - var roleFirstSectionTitleLabel: UILabel! - var roleSecondSectionFlowLayout: FlowAutoLayoutContainer! - var roleSecondSectionTitleLabel: UILabel! - - var personalityContainer: UIView! - var personalityFlowLayout: FlowAutoLayoutContainer! - - var tagsContainer: UIView! - var tagsFlowLayout: FlowAutoLayoutContainer! - - - - let startIndexOfRoleFirst = 5000 - let startIndexOfRoleSecond = 5100 - let startIndexOfPersonal = 5200 - let startIndexOfTags = 5300 - - - // Data. - @Published var selectRoleCode: String = "" - @Published var selectCharacterCode: String = "" - @Published var selectTagCode: String = "" - var selectCharacterNode : DictNode! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupDatas() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - stepLabel = { - let v = UILabel() - v.font = CLSystemToken.font(token: .tlm) - v.textColor = .c.ctsn - v.text = "1/4" - titleView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(titleView.titleLabel) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - // Role - roleContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - - let titleLabel = UILabel() - titleLabel.textColor = .c.ctpn - titleLabel.font = CLSystemToken.font(token: .ttl) - v.addSubview(titleLabel) - titleLabel.text = "Role" - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalToSuperview() - } - - let subTitle = UILabel() - roleFirstSectionTitleLabel = subTitle - subTitle.textColor = .c.ctpn - subTitle.font = CLSystemToken.font(token: .tlm) - v.addSubview(subTitle) - subTitle.text = "Original" - subTitle.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(16) - } - - roleFirstSectionFlowLayout = FlowAutoLayoutContainer() - v.addSubview(roleFirstSectionFlowLayout) - roleFirstSectionFlowLayout.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(subTitle.snp.bottom).offset(12) - // make.bottom.equalToSuperview() - } - - let subTitle2 = UILabel() - roleSecondSectionTitleLabel = subTitle2 - subTitle2.textColor = .c.ctpn - subTitle2.font = CLSystemToken.font(token: .tlm) - v.addSubview(subTitle2) - subTitle2.text = "Fanfiction" - subTitle2.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalTo(roleFirstSectionFlowLayout.snp.bottom).offset(12) - } - - roleSecondSectionFlowLayout = FlowAutoLayoutContainer() - v.addSubview(roleSecondSectionFlowLayout) - roleSecondSectionFlowLayout.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(subTitle2.snp.bottom).offset(12) - make.bottom.equalToSuperview() - } - - return v - }() - - personalityContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - - let titleLabel = UILabel() - titleLabel.textColor = .c.ctpn - titleLabel.font = CLSystemToken.font(token: .ttl) - v.addSubview(titleLabel) - titleLabel.text = "Personality" - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalToSuperview() - } - - personalityFlowLayout = FlowAutoLayoutContainer() - v.addSubview(personalityFlowLayout) - personalityFlowLayout.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.bottom.equalToSuperview() - } - return v - }() - - tagsContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - - let titleLabel = UILabel() - titleLabel.textColor = .c.ctpn - titleLabel.font = CLSystemToken.font(token: .ttl) - v.addSubview(titleLabel) - titleLabel.text = "Tags" - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalToSuperview() - } - - tagsFlowLayout = FlowAutoLayoutContainer() - v.addSubview(tagsFlowLayout) - tagsFlowLayout.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.bottom.equalToSuperview() - } - return v - }() - -// setupRoleItems() -// setupPersonalityItems() -// setupTagsItem() - - titleView.title = "Classification" - bottomButton.setTitle("Next", for: .normal) - } - - private func setupDatas() { - guard let aiDict = AppDictManager.shared.aiDict else { return } - - setupRoleItems(nodes: aiDict.roleDictList) - - setupFlowLayoutItems(nodes: aiDict.characterDictList, flowlayout: personalityFlowLayout) - - //setupFlowLayoutItems(nodes: aiDict.tagDictList, flowlayout: tagsFlowLayout) - } - - private func setupRoleItems(nodes: [DictNode]?) { - guard let roles = nodes else { return } - if roles.count > 0 { - let thisNode = roles[0] - if let childs = thisNode.childDictList { - for (index, per) in childs.enumerated() { - let button = createChipButtonBy(name: per.name) - button.tag = startIndexOfRoleFirst + index - roleFirstSectionFlowLayout.addArrangedSubview(button) - if index == 0 { - selectRoleCode = per.code! - button.isSelected = true - } - } - } - roleFirstSectionTitleLabel.text = thisNode.name - } - - if roles.count > 1 { - let thisNode = roles[1] - if let childs = thisNode.childDictList { - for (index, per) in childs.enumerated() { - let button = createChipButtonBy(name: per.name) - button.tag = startIndexOfRoleSecond + index - roleSecondSectionFlowLayout.addArrangedSubview(button) - } - } - roleSecondSectionTitleLabel.text = thisNode.name - } - } - - private func setupFlowLayoutItems(nodes: [DictNode]?, flowlayout: FlowAutoLayoutContainer) { - guard let realNodes = nodes else { return } - - flowlayout.removeAllArrangedSubviews() - - for (index, per) in realNodes.enumerated() { - let button = createChipButtonBy(name: per.name) - - flowlayout.addArrangedSubview(button) - - // Default select first node - if index == 0 { - if flowlayout == personalityFlowLayout { - selectCharacterCode = per.code! - selectCharacterNode = per - - // 默认选中当前Tags下的第一个tag - setupFlowLayoutItems(nodes: per.childDictList, flowlayout: tagsFlowLayout) - } else if flowlayout == tagsFlowLayout { - selectTagCode = per.code! - } - button.isSelected = true - } - - // Set tag for button - if flowlayout == personalityFlowLayout { - button.tag = startIndexOfPersonal + index - - }else if flowlayout == tagsFlowLayout { - button.tag = startIndexOfTags + index - flowlayout.setNeedsDisplay() - flowlayout.layoutIfNeeded() - setNeedsDisplay() - } - } - - - } - - // MARK: - Public - - func restoreSelectPerson(selectCode: String?){ - guard let aiDict = AppDictManager.shared.aiDict, let code = selectCode, let characterDictList = aiDict.characterDictList else {return} - selectCharacterCode = code - for per in characterDictList { - if code == (per.code ?? "") { - selectCharacterNode = per - } - } - // 其他按钮取消选中 - for per in personalityFlowLayout.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - - for (index, per) in characterDictList.enumerated() { - if per.code == code { - let button = personalityFlowLayout.viewWithTag(startIndexOfPersonal + index) as? EPChipFilterButton - button?.isSelected = true - } - } - } - - func restoreSelectTag(selectCode: String?){ - guard let aiDict = AppDictManager.shared.aiDict, let code = selectCode, let tagDictList = selectCharacterNode.childDictList else {return} - selectTagCode = code - // 其他按钮取消选中 - for per in tagsFlowLayout.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - - for (index, per) in tagDictList.enumerated() { - if per.code == code { - let button = tagsFlowLayout.viewWithTag(startIndexOfTags + index) as? EPChipFilterButton - button?.isSelected = true - } - } - } - - func restoreSelectRole(selectCode: String?){ - guard let aiDict = AppDictManager.shared.aiDict, let code = selectCode, let roleDictList = aiDict.roleDictList else {return} - - selectRoleCode = code - // 其他按钮取消选中 - for per in roleFirstSectionFlowLayout.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - - for per in roleSecondSectionFlowLayout.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - - for (index, per) in roleDictList.enumerated() { - if index == 0{ - if let nodes = per.childDictList{ - for (indexNode, perNode) in nodes.enumerated() { - if perNode.code == code { - let button = roleFirstSectionFlowLayout.viewWithTag(startIndexOfRoleFirst + indexNode) as? EPChipFilterButton - button?.isSelected = true - } - } - } - }else if index == 1{ - if let nodes = per.childDictList{ - for (indexNode, perNode) in nodes.enumerated() { - if perNode.code == code { - let button = roleSecondSectionFlowLayout.viewWithTag(startIndexOfRoleSecond + indexNode) as? EPChipFilterButton - button?.isSelected = true - } - } - } - } - } - -// for (index, per) in roleDictList.enumerated() { -// if per.code == code { -// let button = roleFirstSectionFlowLayout.viewWithTag(startIndexOfRoleFirst + index) as? EPChipFilterButton -// button?.isSelected = true -// }else if per.code == code { -// let button = roleSecondSectionFlowLayout.viewWithTag(startIndexOfRoleSecond + index) as? EPChipFilterButton -// button?.isSelected = true -// } -// } - } - - - // MARK: - Helper - - private func createChipButtonBy(name: String) -> EPChipFilterButton { - let button = EPChipFilterButton() - button.text = name - button.addTarget(self, action: #selector(tapChipButton(sender:)), for: .touchUpInside) - return button - } - - // MARK: - Button - - @objc private func tapChipButton(sender: UIButton) { - guard let superView: FlowAutoLayoutContainer = sender.superview as? FlowAutoLayoutContainer else { - return - } - - guard let aiDict = AppDictManager.shared.aiDict else { return } - - if superView == roleFirstSectionFlowLayout || superView == roleSecondSectionFlowLayout{ - // Reset - for per in roleFirstSectionFlowLayout.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - for per in roleSecondSectionFlowLayout.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - - let index = superView.subviews.firstIndex(of: sender) ?? 0 - sender.isSelected = true - - if superView == roleFirstSectionFlowLayout{ - let node = aiDict.roleDictList![0].childDictList![index] - selectRoleCode = node.code ?? "" - }else if superView == roleSecondSectionFlowLayout{ - let node = aiDict.roleDictList![1].childDictList![index] - selectRoleCode = node.code ?? "" - } - - }else if superView == personalityFlowLayout{ - for per in superView.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - let index = superView.subviews.firstIndex(of: sender) ?? 0 - dlog("当前选中\(index) in \(superView)") - sender.isSelected = true - - selectCharacterCode = aiDict.characterDictList![index].code ?? "" - selectCharacterNode = aiDict.characterDictList![index] - - - - setupFlowLayoutItems(nodes: selectCharacterNode.childDictList, flowlayout: tagsFlowLayout) - }else if superView == tagsFlowLayout{ - for per in superView.subviews { - if let button = per as? EPChipFilterButton { - button.isSelected = false - } - } - let index = superView.subviews.firstIndex(of: sender) ?? 0 - dlog("当前选中\(index) in \(superView)") - sender.isSelected = true - - selectTagCode = selectCharacterNode.childDictList![index].code ?? "" - } - - // Reset -// for per in superView.subviews { -// if let button = per as? EPChipFilterButton { -// button.isSelected = false -// } -// } -// let index = superView.subviews.firstIndex(of: sender) ?? 0 -// dlog("当前选中\(index) in \(superView)") -// sender.isSelected = true - } - - // MARK: - Test - - private func setupTestRoleItems() { - do { - let button = createChipButtonBy(name: "Original") - roleFirstSectionFlowLayout.addArrangedSubview(button) - } - - do { - let button = createChipButtonBy(name: "Anime") - roleFirstSectionFlowLayout.addArrangedSubview(button) - } - - do { - let button = createChipButtonBy(name: "Games") - roleSecondSectionFlowLayout.addArrangedSubview(button) - } - - do { - let button = createChipButtonBy(name: "Film & TV") - roleFirstSectionFlowLayout.addArrangedSubview(button) - } - - do { - let button = createChipButtonBy(name: "CC & GG") - roleFirstSectionFlowLayout.addArrangedSubview(button) - } - - do { - let button = createChipButtonBy(name: "Film") - roleFirstSectionFlowLayout.addArrangedSubview(button) - } - - do { - let button = createChipButtonBy(name: "FSSF_d") - roleFirstSectionFlowLayout.addArrangedSubview(button) - } - roleFirstSectionFlowLayout.layoutIfNeeded() - } - - private func setupTestPersonalityItems() { - do { - let button = createChipButtonBy(name: "Madness") - personalityFlowLayout.addArrangedSubview(button) - } - do { - let button = createChipButtonBy(name: "Calm") - personalityFlowLayout.addArrangedSubview(button) - } - do { - let button = createChipButtonBy(name: "Anime") - personalityFlowLayout.addArrangedSubview(button) - } - do { - let button = createChipButtonBy(name: "Calm2") - personalityFlowLayout.addArrangedSubview(button) - } - do { - let button = createChipButtonBy(name: "Test") - personalityFlowLayout.addArrangedSubview(button) - } - personalityContainer.layoutIfNeeded() - } - - private func setupTestTagsItem() { - do { - let button = createChipButtonBy(name: "Madness") - tagsFlowLayout.addArrangedSubview(button) - } - do { - let button = createChipButtonBy(name: "Calm") - tagsFlowLayout.addArrangedSubview(button) - } - tagsFlowLayout.layoutIfNeeded() - } -} diff --git a/crush/Crush/Src/Modules/Role/Create/View/RoleCreateAppearanceResultCell.swift b/crush/Crush/Src/Modules/Role/Create/View/RoleCreateAppearanceResultCell.swift deleted file mode 100644 index f6d1c7c..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/RoleCreateAppearanceResultCell.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// RoleCreateAppearanceResultCell.swift -// Crush -// -// Created by Leon on 2025/9/24. -// - -/// 形象生成图Cell (包括生成中、队列中、生成成功状态) -class RoleCreateAppearanceResultCell: UICollectionViewCell { - var block: UIView! - var iv: CLImageView! - - var selectMark: UIImageView! - var viewFullButon: EPIconTertiaryDarkButton! - - var photoStateView: PhotoGenerateStateOverlay! - - var tapFullBrowseAction: ((_ data: AIUserImageQuery, _ image: UIImage?) -> Void)? - - var data: AIUserImageQuery? - - override init(frame: CGRect) { - super.init(frame: frame) - setupBaseViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - bringSubviewToFront(photoStateView) - } - - private func setupBaseViews() { - block = { - let v = UIView() - v.backgroundColor = .c.csen - v.layer.cornerRadius = 12 - v.clipsToBounds = true - v.layer.borderWidth = 1 - v.layer.borderColor = UIColor.c.cpn.cgColor - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - iv = { - let v = CLImageView(frame: .zero) - v.layer.cornerRadius = 12 - v.clipsToBounds = true - v.contentMode = .scaleAspectFill - v.backgroundColor = .c.csbn - block.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - selectMark = { - let v = UIImageView() - v.image = UIImage(named: "checkmark_tick") - block.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-8) - make.top.equalToSuperview().offset(16) - } - return v - }() - - viewFullButon = { - let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .small, iconCode: .iconFullimage) - v.addTarget(self, action: #selector(tapViewFullButton), for: .touchUpInside) - block.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-12) - make.bottom.equalToSuperview().offset(-12) - } - return v - }() - - photoStateView = { - let v = PhotoGenerateStateOverlay() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - v.isHidden = true - return v - }() - - layoutIfNeeded() - setupSelected(false) - } - - public func config(data: AIUserImageQuery) { - self.data = data - let state = data.status - - viewFullButon.isHidden = true - iv.loadImage(nil) - - switch state { - case .pending: - photoStateView.showLoading() - case .completed: - photoStateView.hideLoading() - photoStateView.isHidden = true - if let url = data.imageUrl { - iv.loadImage(url) - viewFullButon.isHidden = false - } - // 根据选择情况 - case .failed, .nsfw: - setupSelected(false) - photoStateView.showFailed() - } - } - - func setupSelected(_ selected: Bool) { - if selected { - selectMark.isHidden = false - block.layer.borderWidth = 2 - } else { - selectMark.isHidden = true - block.layer.borderWidth = 0 - } - } - - // MARK: - Action - - @objc private func tapViewFullButton() { - tapFullBrowseAction?(data!, iv.image) - } - - // MARK: private - - override func prepareForReuse() { - super.prepareForReuse() - iv.loadImage(nil) - } - -// class RoleLoadingContainer: UIView{ -// override func layoutSubviews() { -// super.layoutSubviews() -// addDashedBorder(to: self, cornerRadius: 16) -// } -// } -} diff --git a/crush/Crush/Src/Modules/Role/Create/View/RoleCreateAppearanceResultView.swift b/crush/Crush/Src/Modules/Role/Create/View/RoleCreateAppearanceResultView.swift deleted file mode 100644 index 83ecd6b..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/RoleCreateAppearanceResultView.swift +++ /dev/null @@ -1,248 +0,0 @@ -// -// RoleCreateAppearanceResultView.swift -// Crush -// -// Created by Leon on 2025/9/19. -// - -import UIKit -import Combine -/// 形象生成结果页 -class RoleCreateAppearanceResultView: UIView, UICollectionViewDataSource, UICollectionViewDelegate { - var titleLabel: UILabel! - var generateButton: TextButton! - - var stackV : UIStackView! - var warningView : TipWarningView! - var layout : UICollectionViewFlowLayout! - var cv: UICollectionView! - -// var placeholderDatas - /// 编辑时,之前使用中的图像 - @Published var usingFigureImage: AIUserImageQuery? - - var datas: [AIUserImageQuery] = [] - - @Published var selectDatas: [AIUserImageQuery] = [] - @Published var selectImageUrl: String = "" - - private var cancellables = Set() - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - titleLabel = { - let v = UILabel() - v.font = .t.tlm - v.textColor = .c.ctpn - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview().offset(CGFloat.lrs) - } - return v - }() - titleLabel.text = "Appearance Result" - - generateButton = { - let v = TextButton() - v.text = "Regenerate" - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.centerY.equalTo(titleLabel) - make.height.equalTo(40) - } - return v - }() - - stackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 12 - v.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - warningView = { - let v = TipWarningView() - stackV.addArrangedSubview(v) - v.contentStr = "Please wait patiently in the queue, 15 missions ahead" - v.isHidden = false - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - v.isHidden = true - return v - }() - - cv = { - layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.itemSize = CGSize(width: 150, height: 200) - layout.minimumLineSpacing = 12 - layout.minimumInteritemSpacing = 12 - layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12) - - let v = UICollectionView(frame: .zero, collectionViewLayout: layout) - v.backgroundColor = .clear - v.showsHorizontalScrollIndicator = false - v.showsVerticalScrollIndicator = false - v.register(RoleCreateAppearanceResultCell.self, forCellWithReuseIdentifier: "RoleCreateAppearanceResultCell") - v.dataSource = self - v.delegate = self - v.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12) - //addSubview(v) - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.height.equalTo(200) - } - return v - }() - } - - private func setupData(){ - for _ in 0...5 { - datas.append(AIUserImageQuery()) - } - cv.reloadData() - } - - private func setupEvent(){ - $usingFigureImage.sink {[weak self] imageQuery in - - if imageQuery != nil{ - self?.datas.removeAll() - - self?.layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12) - self?.cv.contentInset = UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 12) - - self?.cv.reloadData() - } - }.store(in: &cancellables) - } - - // MARK: - Public - public func config(_ datas: [AIUserImageQuery]?, reset: Bool = false) { - if let validDatas = datas, validDatas.count >= 1{ - self.datas = validDatas - }else{ - // 填充占位数据,如果datas的数量不够6个,补齐datas的数量为6个,补充AIUserImageQuery() - self.datas.removeAll(where: {$0.status != .completed}) - if self.datas.count < 6{ - for _ in 0...5 - self.datas.count { - self.datas.append(AIUserImageQuery()) - } - } - } - - if reset{ - selectDatas.removeAll() - selectImageUrl = "" - } - - cv.reloadData() - } - - public func loadDataAndSelect(_ data: AIUserImageQuery?){ - guard let outImage = data else {return} - - datas = [] - usingFigureImage = outImage - selectDatas = [outImage] - selectImageUrl = outImage.imageUrl ?? "" - - cv.reloadData() - } - - public func setupLastGenerateProcessStart(){ - generateButton.isEnabled = false - } - - public func setupLastGenerateProcessEnd(){ - generateButton.isEnabled = true - } - - // MARK: - UICollectionViewDataSource - - func numberOfSections(in collectionView: UICollectionView) -> Int { - return 2 - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if section == 0{ - return usingFigureImage != nil ? 1 : 0 - } - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - var item: AIUserImageQuery! - if indexPath.section == 0{ - item = usingFigureImage ?? AIUserImageQuery() - }else{ - item = datas[indexPath.item] - } - - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoleCreateAppearanceResultCell", for: indexPath) as! RoleCreateAppearanceResultCell - - cell.config(data: item) - if let url = item.imageUrl, url == selectImageUrl { - cell.setupSelected(true) - } else { - cell.setupSelected(false) - } - - cell.tapFullBrowseAction = { data, image in - guard let url = data.imageUrl, !url.isEmpty else { return } - - var photoModels = [PhotoBrowserModel]() - let model = PhotoBrowserModel() - model.image = image - model.sourceRect = cell.screenRect ?? .zero - model.imageUrl = url - photoModels.append(model) - ImageBrowser.show(models: photoModels, index: 0, type: .normal) - } - - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - var item: AIUserImageQuery! - if indexPath.section == 0{ - guard let using = usingFigureImage else{ - return - } - item = using - }else{ - item = datas[indexPath.item] - } - - let status = item.status - guard status == .completed, let url = item.imageUrl else { return } - - selectImageUrl = url - selectDatas.removeAll() - selectDatas.append(item) - selectDatas = selectDatas - collectionView.reloadData() - } -} - diff --git a/crush/Crush/Src/Modules/Role/Create/View/RoleDialogStyleSetView.swift b/crush/Crush/Src/Modules/Role/Create/View/RoleDialogStyleSetView.swift deleted file mode 100644 index 032d939..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/RoleDialogStyleSetView.swift +++ /dev/null @@ -1,245 +0,0 @@ -// -// RoleDialogStyleSetView.swift -// Crush -// -// Created by Leon on 2025/7/21. -// -import UIKit -import Combine -class RoleDialogStyleSetView: UIView { - var backButton: EPIconTertiaryButton! - var bottomButton: StyleButton! - var container: LTScrollContainer! - var titleView: TitleView! - var stepLabel: UILabel! - - var styleTextView: TitleTextView! - var openingTextView: TitleTextView! - var vocieSelectView: CLSelectView! - - var generate1Button: TextButton! - var generate2Button: TextButton! - - // Data - - @Published var dialogStyleContent: String = "" - @Published var dialogStyleAIGenerated: String = "" - - @Published var openingContent: String = "" - @Published var openingAIGenerated: String = "" - - @Published var canGoNext: Bool = false - - @Published var selectVoice: VoiceModel? - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .large, iconCode: .arrowLeftBorder) - let size = v.bgImageSize() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - make.size.equalTo(CGSize(width: 88, height: size.height)) - } - return v - }() - - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - // make.leading.equalToSuperview().offset(CGFloat.lrs) - make.leading.equalTo(backButton.snp.trailing).offset(16) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - stepLabel = { - let v = UILabel() - v.font = CLSystemToken.font(token: .tlm) - v.textColor = .c.ctsn - v.text = "3/4" - titleView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(titleView.titleLabel) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - - do{ - openingTextView = { - let v = TitleTextView() - v.maxLimit = 150 - v.minLimit = 10 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - generate2Button = { - let v = TextButton() - v.text = "自动生成" - openingTextView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalTo(openingTextView.titleLabel) - make.height.equalTo(40) - } - return v - }() - } - - do{ - styleTextView = { - let v = TitleTextView() - v.titleAppendOptionalLabel() - v.maxLimit = 300 - v.minLimit = 10 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - generate1Button = { - let v = TextButton() - v.text = "自动生成" - styleTextView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalTo(styleTextView.titleLabel) - make.height.equalTo(40) - } - return v - }() - } - - vocieSelectView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - titleView.title = "Dialog" - styleTextView.titleLabel.text = "Dialogue style" - styleTextView.placeholder = "请描述角色的聊天方式,对话语气" - styleTextView.errorMsg = "只能包含10-300字符" - - openingTextView.titleLabel.text = "Opening remarks" - openingTextView.placeholder = "请描述角色的开场白" - openingTextView.errorMsg = "只能包含10-150字符" - vocieSelectView.titleLabel.text = "Voice" - vocieSelectView.placeholder = "选择角色语音音色" - bottomButton.setTitle("Next", for: .normal) - } - - private func setupData(){ - - } - - private func setupEvent(){ - $canGoNext.sink {[weak self] can in - self?.bottomButton.isEnabled = can - }.store(in: &cancellables) - - styleTextView.textDidChanged = {[weak self] textfield in - self?.dialogStyleContent = textfield.text - } - - openingTextView.textDidChanged = {[weak self] textfield in - self?.openingContent = textfield.text - } - - $dialogStyleContent.sink {[weak self] str in - self?.styleTextView.defaultText = str - }.store(in: &cancellables) - - $openingContent.sink {[weak self] str in - self?.openingTextView.defaultText = str - }.store(in: &cancellables) - - $dialogStyleAIGenerated.sink {[weak self] str in - //self?.styleTextView.defaultText = str - self?.dialogStyleContent = str - }.store(in: &cancellables) - - $openingAIGenerated.sink {[weak self] str in - //self?.openingTextView.defaultText = str - self?.openingContent = str - }.store(in: &cancellables) - - $selectVoice.sink {[weak self] timber in - self?.vocieSelectView.contentStr = timber?.voiceDict.name - }.store(in: &cancellables) - - Publishers.CombineLatest3($dialogStyleContent.prepend(""), $openingContent.prepend(""), $selectVoice.prepend(nil)).map{ - style, opening, voice in - if opening.trimmed.count > 0, voice != nil{ - return true - } - return false - }.assign(to: &$canGoNext) - - #if DEBUG - testData() - #endif - } - - - private func testData(){ - openingContent = "嗨,我是你的好朋友,随时准备为你提供帮助!不管你是有一个具体问题,还是只是想聊聊,都可以直接告诉我哦。我们开始吧 😊" - dialogStyleContent = "" // 可以为空 - //"这个角色以自然、亲切而专业的语气进行对话。它能够快速理解用户的意图,回复逻辑清晰,表达简洁明了。语气不过于拘谨,也不过于随意,保持尊重同时具有一定的亲和力。当用户感到困惑或提出问题时,它总是耐心解释,适时提出引导性的建议,营造轻松、高效的沟通氛围。" - } -} diff --git a/crush/Crush/Src/Modules/Role/Create/View/RoleFigureGenerateView.swift b/crush/Crush/Src/Modules/Role/Create/View/RoleFigureGenerateView.swift deleted file mode 100644 index 4e8a3ea..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/RoleFigureGenerateView.swift +++ /dev/null @@ -1,408 +0,0 @@ -// -// RoleFigureGenerateView.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import UIKit -import Combine -class RoleFigureGenerateView: UIView { - var bottomButton: StyleButton! - var container: LTScrollContainer! - var titleView: TitleView! - - // Stackview's subviews - var styleContainer:UIView! - var styleTipLabel:UILabel! - var styleCv:UICollectionView! - - var descriptionTextView: TitleTextView! - var generate1Button: TextButton! - - // 参考图 - var referenceContainer:UIView! - var referenceTipLabel:UILabel! - var referenceButton : CommonUploadImageButton! - - var datas:[ImageStylePic] = [] - @Published var selectStyleId: String = "" - var selectStylePrompt: String = "" - - @Published var figureDescriptionAIGenerated: String = "" - @Published var figureDescriptionContent: String = "" - - @Published var buttonEnable:Bool = false - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - v.setTitle("Generate", for: .normal) - return v - }() - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 16 - v.optionInnerTopPadding = 16 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - v.title = "Appearance" - return v - }() - container.stack.setCustomSpacing(0, after: titleView) - - styleContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - styleTipLabel = { - let v = UILabel() - styleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview().offset(12) - } - v.font = .t.tlm - v.textColor = .c.ctpn - v.text = "Style" - return v - }() - - styleCv = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - let size = CGSize(width: 100, height: 100 + 24) - layout.itemSize = size - layout.minimumLineSpacing = 12 - layout.minimumInteritemSpacing = 12 - layout.sectionInset = UIEdgeInsets(top: 0, left: CGFloat.lrs, bottom: 0, right: CGFloat.lrs) - let v = UICollectionView(frame: .zero, collectionViewLayout: layout) - styleContainer.addSubview(v) - v.showsHorizontalScrollIndicator = false - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(styleTipLabel.snp.bottom).offset(12) - make.height.equalTo(size.height) - make.bottom.equalToSuperview() - } - v.backgroundColor = .clear - v.register(RoleFigureStyleCell.self, forCellWithReuseIdentifier: "RoleFigureStyleCell") - v.dataSource = self - v.delegate = self - return v - }() - - descriptionTextView = { - let v = TitleTextView() - // 10~500 - v.maxLimit = 1000 -// v.minLimit = 10 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - generate1Button = { - let v = TextButton() - v.text = "自动生成" - descriptionTextView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalTo(descriptionTextView.titleLabel) - make.height.equalTo(40) - } - return v - }() - - referenceContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - //make.leading.equalToSuperview().offset(CGFloat.lrs) - //make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - } - return v - }() - - referenceTipLabel = { - let v = UILabel() - referenceContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.top.equalToSuperview() - } - v.font = .t.tlm - v.textColor = .c.ctpn - v.text = "Reference" - return v - }() - - let optionalLabel = { - let v = EPOptionFlagLabel() - referenceContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(referenceTipLabel.snp.trailing).offset(4) - make.centerY.equalTo(referenceTipLabel) - } - return v - }() - optionalLabel.isHidden = false - - referenceButton = { - let v = CommonUploadImageButton() - referenceContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - //make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalTo(referenceTipLabel.snp.bottom).offset(8) - make.height.equalTo(100) - make.width.equalTo(100) - make.bottom.equalToSuperview() - } - return v - }() - - - referenceContainer.isHidden = true - - descriptionTextView.titleLabel.text = "Description" - descriptionTextView.placeholder = "请描述形象的肤色、服饰、发型、五官、动作、背景等" -// descriptionTextView.errorMsg = "只能包含10-500字符" - } - - func setupData(){ - - } - - private func setupEvent(){ - - - $buttonEnable.sink {[weak self] enable in - self?.bottomButton.isEnabled = enable - }.store(in: &cancellables) - - $figureDescriptionContent.sink {[weak self] str in - self?.descriptionTextView.defaultText = str - }.store(in: &cancellables) - - $figureDescriptionAIGenerated.sink {[weak self] str in - //self?.descriptionTextView.defaultText = str - self?.figureDescriptionContent = str - }.store(in: &cancellables) - - $selectStyleId.sink {[weak self] selectStyleId in - guard let theDatas = self?.datas else{ - return - } - for per in theDatas{ - if selectStyleId == per.code ?? ""{ - self?.selectStylePrompt = per.prompt ?? "" - } - } - }.store(in: &cancellables) - - descriptionTextView.textDidChanged = {[weak self] textField in - self?.figureDescriptionContent = textField.text - } - - Publishers.CombineLatest($figureDescriptionContent, $selectStyleId).map{ - content, selectStyleCode in - if content.trimmed.count > 0 && selectStyleCode.count > 0{ - return true - } - return false - }.assign(to: &$buttonEnable) - -//#if DEBUG -//figureDescriptionContent = "beautiful high school girl, elegant rich family daughter, medium academic performance, refined but slightly rebellious look, layered personality, fashionable school uniform with subtle luxury accessories, natural makeup, confident yet thoughtful expression, long silky hair, warm afternoon sunlight, cinematic composition, highly detailed, 8k, ultra realistic, anime-inspired style" -//#endif - - setupStylesDatas() - } - - func setupStylesDatas(){ - if let imageStyles = AppDictManager.shared.aiDict?.imageStyleDictList{ - datas = imageStyles - // 默认选中的话,请注释这段代码 - if let first = datas.first{ - selectStyleId = first.code ?? "" - selectStylePrompt = first.prompt ?? "" - } - }else{ - Hud.showIndicator() - AppDictManager.shared.loadAIDict {[weak self] ok in - Hud.hideIndicator() - if(ok){ - self?.setupStylesDatas() - } - } - } - } - - // MARK: - Public - - func scrollSelectedShow(){ - // 将选中的style的cell滑动显示出来 - if let index = self.datas.firstIndex(where: {$0.code == selectStyleId}){ - // print("currentindex : \(index)") - self.styleCv.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: false) - } - } - -} - -extension RoleFigureGenerateView: UICollectionViewDataSource, UICollectionViewDelegate{ - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoleFigureStyleCell", for: indexPath) as! RoleFigureStyleCell - let item = datas[indexPath.item] - cell.setupData(data: item) - cell.setupSelected(selected: item.code == selectStyleId) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let item = datas[indexPath.item] - selectStyleId = item.code ?? "" - selectStylePrompt = item.prompt ?? "" - collectionView.reloadData() - } -} - -class RoleFigureStyleCell: UICollectionViewCell{ - var block:UIView! - var iv:UIImageView! - var titleLabel:UILabel! - var selectedMark:UIImageView! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews(){ - block = { - let v = UIView() - v.backgroundColor = .c.csen - v.layer.borderColor = UIColor.c.cpn.cgColor - v.layer.borderWidth = 0 - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalToSuperview() - make.height.equalTo(v.snp.width) - } - return v - }() - - iv = { - let v = UIImageView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = UILabel() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(block.snp.bottom).offset(4) - } - v.font = .t.tls - v.textColor = .c.ctpn - v.text = "Style" - v.textAlignment = .center - return v - }() - - selectedMark = { - let v = UIImageView() - v.image = UIImage(named: "checkmark_tick") - - block.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-8) - make.top.equalToSuperview().offset(8) - } - v.isHidden = true - return v - }() - } - - func setupSelected(selected:Bool){ - selectedMark.isHidden = !selected - block.layer.borderWidth = selected ? 2 : 0 - } - - func setupData(data:ImageStylePic){ - titleLabel.text = data.name - iv.loadImage(data.url) - } -} - - diff --git a/crush/Crush/Src/Modules/Role/Create/View/RolePublishView.swift b/crush/Crush/Src/Modules/Role/Create/View/RolePublishView.swift deleted file mode 100644 index b04c0c2..0000000 --- a/crush/Crush/Src/Modules/Role/Create/View/RolePublishView.swift +++ /dev/null @@ -1,473 +0,0 @@ -// -// RolePublishView.swift -// Crush -// -// Created by Leon on 2025/7/21. -// -import Combine -import Lottie -import UIKit -class RolePublishView: UIView { - var backButton: EPIconTertiaryButton! - var bottomButton: StyleButton! - var container: LTScrollContainer! - var titleView: TitleView! - var stepLabel: UILabel! - - // Option 1 - var uploadPicContainer: UIView! - var uploadImageView: UploadImageView! - - // Option 2 - var appearanceResult: RoleCreateAppearanceResultView! - var avatarView: RoleCreateAvatarView! - - var introduction: TitleTextView! - var generate1Button: TextButton! - - var privacyPublicButton: EPChipAssistButton! - var privacyPrivateButton: EPChipAssistButton! - - // Data - @Published var buttonEnable: Bool = false - - @Published var privacyPublic: Bool = true - @Published var introductionContent: String = "" - @Published var contentAIGenerated: String = "" - - var tempAvatar: UploadPhotoM? - @Published var avatarModel: UploadPhotoM? - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupDatas() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - // MARK: - Views - private func setupViews() { - backButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .large, iconCode: .arrowLeftBorder) - let size = v.bgImageSize() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - make.size.equalTo(CGSize(width: 88, height: size.height)) - } - return v - }() - - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - // make.leading.equalToSuperview().offset(CGFloat.lrs) - make.leading.equalTo(backButton.snp.trailing).offset(16) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - stepLabel = { - let v = UILabel() - v.font = CLSystemToken.font(token: .tlm) - v.textColor = .c.ctsn - v.text = "4/4" - titleView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(titleView.titleLabel) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - // Option1 - setupAppearance() - // Option 2 - setupAppearanceResult() - - setupAvatar() - - introduction = { - let v = TitleTextView() - // 10~300 - v.maxLimit = 300 - v.minLimit = 10 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - generate1Button = { - let v = TextButton() - v.text = "自动生成" - introduction.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalTo(introduction.titleLabel) - make.height.equalTo(40) - } - return v - }() - - - setupPrivacy() - - //itleView.title = "Create" - introduction.titleLabel.text = "Introduction" - introduction.placeholder = "为用户介绍该虚拟角色" - introduction.errorMsg = "只能包含10-300字符" - //bottomButton.setTitle("Create", for: .normal) - } - - private func setupAppearance() { - uploadPicContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - let titleLabel = { - let v = UILabel() - v.font = .t.tlm - v.textColor = .c.ctpn - uploadPicContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - } - return v - }() - - uploadImageView = { - let v = UploadImageView() - uploadPicContainer.addSubview(v) - v.snp.makeConstraints { make in - // w:h = 345:200 - make.height.equalTo(v.snp.width).multipliedBy(200.0 / 345.0) - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } - return v - }() - - titleLabel.text = "Appearance" - } - - private func setupAppearanceResult() { - appearanceResult = { - let v = RoleCreateAppearanceResultView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - } - return v - }() - appearanceResult.isHidden = true - } - - private func setupAvatar() { - avatarView = RoleCreateAvatarView() - container.stack.addArrangedSubview(avatarView) - avatarView.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - avatarView.isHidden = true - } - - // MARK: Privacy - - private func setupPrivacy() { - let view = UIView() - container.stack.addArrangedSubview(view) - view.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - - let titleLabel = { - let v = UILabel() - v.font = .t.tlm - v.textColor = .c.ctpn - view.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - } - return v - }() - - privacyPublicButton = { - let v = EPChipAssistButton() - v.iconCode = .eyeOn - v.text = "Public" - v.defaultIconColor = .c.ctpn - v.addTarget(self, action: #selector(tapPrivacyButton(sender:)), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.bottom.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(12) - } - return v - }() - - privacyPrivateButton = { - let v = EPChipAssistButton() - v.iconCode = .eyeOff - v.text = "Private" - v.defaultIconColor = .c.ctpn - v.addTarget(self, action: #selector(tapPrivacyButton(sender:)), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(privacyPublicButton.snp.trailing).offset(12) - make.top.equalTo(titleLabel.snp.bottom).offset(12) - } - return v - }() - - titleLabel.text = "Privacy" - } - - private func setupDatas(){ - // See more EGImageInputView - uploadImageView.state = .default - } - - private func setupEvent() { - $buttonEnable.sink {[weak self] enable in - self?.bottomButton.isEnabled = enable - }.store(in: &cancellables) - - $privacyPublic.sink { [weak self] result in - self?.privacyPublicButton.isSelected = result - self?.privacyPrivateButton.isSelected = !result - }.store(in: &cancellables) - - $introductionContent.sink { [weak self] str in - self?.introduction.defaultText = str - }.store(in: &cancellables) - - $contentAIGenerated.sink { [weak self] str in - self?.introductionContent = str - }.store(in: &cancellables) - - appearanceResult.$selectImageUrl.sink { [weak self] result in - self?.selectFigureImageUrl(imgUrl: result) - }.store(in: &cancellables) - - $avatarModel.sink {[weak self] model in - if let url = model?.remoteFullPath{ - self?.avatarView.bind(imageUrl: url) - } - }.store(in: &cancellables) - - avatarView.tapCropAction = {[weak self] image in - self?.cropAIAvatar(image: image) - } - - introduction.textView.textPublisher.sink {[weak self] string in - self?.introductionContent = string.trimmed - }.store(in: &cancellables) -// introduction.textDidChanged = {[weak self] clTextView in -// self?.introductionContent = clTextView.text.trimmed -// } - - - - // 选择了形象图 & introduction ok - Publishers.CombineLatest(appearanceResult.$selectImageUrl.prepend(""), $introductionContent.prepend("")).map{ - url, content in - if url.count > 0, content.count >= 10{ - return true - } - return false - }.assign(to: &$buttonEnable) - - - - } - - // MARK: - Public - public func showQueryGeneratedImages(generating: Bool) { - uploadPicContainer.isHidden = generating - appearanceResult.isHidden = generating == false - } - - public func loadDataAndSelect(_ image: AIUserImageQuery?){ - guard let select = image else {return} - - //selectFigureImageUrl(imgUrl: select.imageUrl) - appearanceResult.loadDataAndSelect(select) - - } - - // MARK: - Helper - func selectFigureImageUrl(imgUrl: String?){ - if let url = imgUrl{ - avatarView.isHidden = url.isEmpty - avatarView.bind(imageUrl: url) - - let photo = UploadPhotoM() - photo.setupValidImageUrl(url: url) - avatarModel = photo - } - } - - private func cropAIAvatar(image:UIImage?){ - guard let avatar = image else {return} - - let image = avatar - -// let image = UIImage(named: "egpic")! - let cropViewController = CropViewController(image: image) { [unowned self] croppedImage in - self.avatarView.bindImage(image: croppedImage) - - // 裁剪后的图片 - let photo = UploadPhotoM() - photo.image = croppedImage - photo.addThisItemTimeStamp = Date().timeStamp - avatarModel = photo - } - let navc = CLNavigationController(rootViewController: cropViewController) - UIWindow.getTopViewController()?.present(navc, animated: true, completion: nil) - - } - - // MARK: - Action - - // Action - @objc private func tapPrivacyButton(sender: UIButton) { - privacyPublic = !privacyPublic - } -} - -class RoleCreateAvatarView: UIView { - var titleLabel: UILabel! - var cropButton: TextButton! - var avatarIv: CLImageView! - - var image: UIImage? - - var tapCropAction: ((_ image: UIImage?) -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - titleLabel = { - let v = UILabel() - v.font = .t.tlm - v.textColor = .c.ctpn - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview() - } - return v - }() - - cropButton = { - let v = TextButton() - v.addTarget(self, action: #selector(tapCropButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalTo(titleLabel) - } - v.isEnabled = false - return v - }() - - avatarIv = { - let v = CLImageView(frame: .zero) - v.layer.cornerRadius = 32 - v.clipsToBounds = true - v.contentMode = .scaleAspectFill - v.backgroundColor = .c.csbn - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.size.equalTo(CGSize(width: 64, height: 64)) - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.bottom.equalToSuperview() - } - return v - }() - - titleLabel.text = "Avatar" - cropButton.text = "Crop" - } - - /// 从url获取的完整形象图 - public func bind(imageUrl: String) { - avatarIv.loadImage(imageUrl, completionBlock: { [weak self] result in - switch result { - case let .success(imageResult): - self?.image = imageResult.image - self?.cropButton.isEnabled = true - case .failure: - return - } - }) - } - - public func bindImage(image: UIImage?) { - avatarIv.image = image - cropButton.isEnabled = true - } - - @objc private func tapCropButton(){ - guard let fullFigure = image else{return} - tapCropAction?(fullFigure) - } -} diff --git a/crush/Crush/Src/Modules/Role/Home/RoleHomeAboutController.swift b/crush/Crush/Src/Modules/Role/Home/RoleHomeAboutController.swift deleted file mode 100644 index 84e01e1..0000000 --- a/crush/Crush/Src/Modules/Role/Home/RoleHomeAboutController.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// RoleHomeAboutController.swift -// Crush -// -// Created by Leon on 2025/7/24. -// - -import JXPagingView -import UIKit -class RoleHomeAboutController: CLViewController { - var aiId: Int? - var page: Int = 1 - - var info: AIRoleInfo? - var datas: [AIRoleGiftReceivedList]? - var listViewDidScrollCallback: ((UIScrollView) -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - navigationView.isHidden = true - - container.container.scrollView.delegate = self - } - - private func setupDatas() { - loadStatistics() - loadGifts() - } - - private func setupEvents() { - } - - // MARK: - Public - - func loadStatistics() { - guard let id = aiId else { return } - - AIRoleProvider.request(.aiRoleStatistics(aiId: id), modelType: AIRoleStatisticsResponse.self) { [weak self] result in - switch result { - case let .success(success): - self?.container.configStat(data: success) - case let .failure(failure): - dlog(failure) - } - } - // Load Gifts - } - - func loadGifts() { - guard let id = aiId else { return } - var paramas = [String: Any]() - paramas.updateValue(id, forKey: "aiId") - let page = ["pn": page, "ps": 10] - paramas.updateValue(page, forKey: "page") - AIRoleProvider.request(.aiRoleGotGiftList(params: paramas), modelType: ResponseContentPageData.self) { [weak self] result in - switch result { - case let .success(success): - self?.datas = success?.datas - self?.container.configGifts(data: success?.datas) - case .failure: - break - } - } - } - - func config(info: AIRoleInfo?) { - self.info = info - guard let data = info else { return } - - container.config(info: data) - } -} - -extension RoleHomeAboutController: UIScrollViewDelegate{ - func scrollViewDidScroll(_ scrollView: UIScrollView) { - listViewDidScrollCallback?(scrollView) - } -} - -extension RoleHomeAboutController: JXPagingViewListViewDelegate { - func listView() -> UIView { - return view - } - - func listScrollView() -> UIScrollView { - return container.container.scrollView - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - listViewDidScrollCallback = callback - } -} diff --git a/crush/Crush/Src/Modules/Role/Home/RoleHomeAlbumController.swift b/crush/Crush/Src/Modules/Role/Home/RoleHomeAlbumController.swift deleted file mode 100644 index ff5061b..0000000 --- a/crush/Crush/Src/Modules/Role/Home/RoleHomeAlbumController.swift +++ /dev/null @@ -1,522 +0,0 @@ -// -// RoleHomeAlbumController.swift -// Crush -// -// Created by Leon on 2025/7/24. -// - -import JXPagingView -import SnapKit -import UIKit -import Combine -class RoleHomeAlbumController: CLBaseGridController { - - var aiId: Int? - var userId: Int? - - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - layout.invalidateLayout() - } - - private func setupViews() { - collectionView.snp.remakeConstraints { make in - make.edges.equalToSuperview() - } - - navigationView.isHidden = true - collectionView.register(RoleHomeAlbumGridCell.self, forCellWithReuseIdentifier: "RoleHomeAlbumGridCell") - let itemW = floor((UIScreen.width - 24 * 2 - 16) * 0.5) as CGFloat - let itemH = itemW - // layout = UICollectionViewFlowLayout() - layout.itemSize = CGSize(width: itemW, height: itemH) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 12, left: 24, bottom: 12 + UIWindow.safeAreaBottom, right: 24) - layout.invalidateLayout() - - addRefreshHeader() - addRefreshFooter() - - collectionView.mj_header?.beginRefreshing() - - } - - private func setupDats() { - - } - - override func loadData() { - guard let id = aiId else {return } - let pageData = RequestPageData() - pageData.pn = page - - let pageParams = pageData.toNonNilDictionary() - var params = [String: Any]() - params.updateValue(id, forKey: "aiId") - params.updateValue(pageParams, forKey: "page") - - - AIRoleProvider.request(.aiRoleAlbumList(params: params), modelType: ResponseContentPageData.self) {[weak self] result in - self?.collectionView.mj_header?.endRefreshing() - self?.collectionView.mj_footer?.endRefreshing() - switch result { - case .success(let success): - if let albums = success?.datas{ - if(pageData.pn == 1){ - self?.datas = albums - self?.collectionView.mj_footer?.resetNoMoreData() - self?.view.setupEmpty(empty: albums.count <= 0, msg: "暂无图片") - }else{ - self?.datas.append(contentsOf: albums) - if albums.count <= 0{ - self?.collectionView.mj_footer?.endRefreshingWithNoMoreData() - } - } - self?.collectionView.reloadData() - } - case .failure: - break - } - } - - } - - private func setupEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(notiAlbumsInfoChanged), name: AppNotificationName.aiRoleAlbumPhotoInfoChanged.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notiAlbumsAddOrDeleted), name: AppNotificationName.aiRoleAlbumAddOrDelete.notificationName, object: nil) - - PhotosViewModel.shared.$album.sink {[weak self] photo in - guard let self = self else{return} - guard let album = photo , let albumId = photo?.albumId else {return} - - for per in self.datas{ - guard let perAlbum = per as? AlbumPhotoItem else {return} - if albumId == perAlbum.albumId{ - perAlbum.updateFrom(album) - self.collectionView.reloadData() - break - } - } - }.store(in: &cancellables) - } - - override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoleHomeAlbumGridCell", for: indexPath) as! RoleHomeAlbumGridCell - let data = datas[indexPath.item] - cell.isSelf = UserCore.shared.isSelfByUid(uid: userId) - cell.config(data: data as? AlbumPhotoItem) - return cell - } - - override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let album = datas[indexPath.item] as? AlbumPhotoItem else {return} - -// guard let uid = userId else{return} - -// if album.lockStatus == .locked && !UserCore.shared.isSelf(id: userId){ -// return -// } - - var photoModels = [PhotoBrowserModel]() - - for (index, per) in datas.enumerated() { - guard let perAlbum = per as? AlbumPhotoItem else{ - return - } - let model = PhotoBrowserModel() - model.aiAlbum = perAlbum - model.aiId = aiId - model.imageUrl = perAlbum.getImgUrl() - if index == indexPath.item{ - if let cell = collectionView.cellForItem(at: indexPath) as? RoleHomeAlbumGridCell{ - model.image = cell.imageView.image - model.sourceRect = cell.screenRect ?? .zero - } - } - model.deleteTapBlock = { [weak self] model, completeBlock in - // req api to delete photo -// #warning("test") -// Hud.showIndicator() -// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { -// Hud.hideInidcator() -// completeBlock(true) -// } - - Hud.showIndicator() - AIRoleProvider.request(.aiRoleAlbumDel(userId: self?.userId, albumId: model.aiAlbum.albumId), modelType: EmptyModel.self) { result in - Hud.hideIndicator() - switch result { - case .success: - completeBlock(true) - NotificationCenter.post(name: .aiRoleAlbumAddOrDelete) - case .failure: - completeBlock(false) - } - } - - } - photoModels.append(model) - } - - if UserCore.shared.isSelf(id: userId){ - ImageBrowser.show(models: photoModels, index: indexPath.item, type: .roleMine) - }else{ - ImageBrowser.show(models: photoModels, index: indexPath.item, type: .roleOthersInAlbum) - } - - } - - - @objc private func notiAlbumsInfoChanged(){ - collectionView.mj_header?.beginRefreshing() - } - - @objc private func notiAlbumsAddOrDeleted(){ - collectionView.mj_header?.beginRefreshing() - } -} - -extension RoleHomeAlbumController: JXPagingViewListViewDelegate { - func listView() -> UIView { - return view - } - - func listScrollView() -> UIScrollView { - return collectionView - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - listViewDidScrollCallback = callback - } -} - -class RoleHomeAlbumGridCell: UICollectionViewCell { - var imageView: AutoRatioImageView! - - /// 🚩正常图片 - var normalContainer: UIView! - /// 左上、默认flag - var flagOfDefault: EPTagLabel! - /// 右上角,黑色标识是否带锁(对于自己)。 - var lockFlag: EPIconFlagView! - var unlockedFlag: EPIconPrimaryButton! // 🔓 + 主题背景色 - /// 左下 - var likeView: HeartLikeCountView! - - /// 🚩LockContainer - var lockContainer: UIView! - var unlockCoinLabel: UILabel! - - // Flag - public var usedInMeetCard : Bool = false - // @Required - public var data: AlbumPhotoItem? - // @Required - public var isSelf: Bool = false - - // 添加请求状态标记 - private var isRequesting = false - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // - func config(data:AlbumPhotoItem?){ - self.data = data; - guard let photo = data else {return} - - - //imageView.loadImage(data?.getImgUrl()) - imageView.setImage(with: data?.getImgUrl().urlValue) - - likeView.isLike = photo.likedStatus == .liked - let likedCount = photo.likedCount ?? 0 - let likeCountDisplay = String.displayNumber(NSNumber(value: likedCount), scale: 1) - likeView.countLabel.text = likeCountDisplay// String.displayInteger(photo.likedCount ?? 0) - - if isSelf{ - lockContainer.isHidden = true - normalContainer.isHidden = false - - unlockedFlag.isHidden = true - lockFlag.isHidden = true - if let lockStatus = photo.lockStatus{ - lockFlag.isHidden = lockStatus != .unlock - } - flagOfDefault.isHidden = !photo.isDefault.boolValue - }else{ - flagOfDefault.isHidden = true - lockFlag.isHidden = true - unlockedFlag.isHidden = true - - if let lockStatus = photo.lockStatus{ - if lockStatus == .unlock{ - // 解锁了的 - lockContainer.isHidden = true - normalContainer.isHidden = false - unlockedFlag.isHidden = false - flagOfDefault.isHidden = !photo.isDefault.boolValue - - }else if lockStatus == .locked{ - // 他人带锁 - lockContainer.isHidden = false - normalContainer.isHidden = true - let coin = Coin(cents: (data?.unlockPrice ?? 0)) - unlockCoinLabel.text = "\(coin.formatted) unlock" - } - }else{ - // 未上锁Open - lockContainer.isHidden = true - normalContainer.isHidden = false - flagOfDefault.isHidden = !photo.isDefault.boolValue - } - - if usedInMeetCard{ - flagOfDefault.isHidden = true - } - - } - } - - private func setupViews() { - contentView.backgroundColor = .c.csnn - contentView.layer.cornerRadius = 16 - contentView.layer.masksToBounds = true - - imageView = { - let v = AutoRatioImageView() - v.layer.cornerRadius = 16 - v.layer.masksToBounds = true - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - } - return v - }() - - normalContainer = { - let v = UIView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - flagOfDefault = { - let v = EPTagLabel(style: .blackOnColor) - normalContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(8) - make.top.equalToSuperview().offset(8) - } - return v - }() - - lockFlag = { - let v = EPIconFlagView(frame: .zero) - v.setupRightTopLockStyle() - normalContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - v.isHidden = true - return v - }() - - unlockedFlag = { - let v = EPIconPrimaryButton(radius: .rectangle, iconSize: .xs, iconCode: .iconPublic) - v.layer.cornerRadius = 4 - normalContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - v.isHidden = true - return v - }() - - likeView = { - let v = HeartLikeCountView() - normalContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(8) - make.bottom.equalToSuperview().offset(-8) - } - return v - }() - - setupLockStateViews() - - flagOfDefault.text = "Default" - likeView.countLabel.text = "0" - - - -// #warning("test") -// testData() - } - - private func setupLockStateViews() { - lockContainer = { - let v = UIView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - let stackV = { - let v = UIStackView() - v.axis = .vertical - v.alignment = .center - v.spacing = 16 - lockContainer.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - make.left.greaterThanOrEqualToSuperview() - make.right.lessThanOrEqualToSuperview() - } - return v - }() - - let lockIcon = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .iconPrivate, size: CGSize(width: 24, height: 24), color: .white) - stackV.addArrangedSubview(v) - return v - }() - lockIcon.isHidden = false - - let stackH = { - let v = UIStackView() - v.spacing = 4 - v.alignment = .center - stackV.addArrangedSubview(v) - return v - }() - - let coinIv = { - let v = UIImageView() - v.image = UIImage(named: "icon_16_diamond") - stackH.addArrangedSubview(v) - return v - }() - coinIv.isHidden = false - - unlockCoinLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tlm - stackH.addArrangedSubview(v) - return v - }() - } - - private func setupData(){ - - } - - private func setupEvent(){ - likeView.likeButton.addTarget(self, action: #selector(tapLikeHeart), for: .touchUpInside) - } - - private func testData() { - imageView.image = UIImage(named: "egpic") - unlockCoinLabel.text = "123 unlock" - - lockContainer.backgroundColor = .random - normalContainer.isHidden = true - lockContainer.isHidden = false - } - - @objc private func tapLikeHeart() { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} - - // 防止重复点击 - guard !isRequesting else { return } - guard let album = self.data else { return } - - let albumId = album.albumId - isRequesting = true - - // 禁用按钮 - likeView.likeButton.isEnabled = false - - if album.likedStatus == .liked { - // 取消点赞 - album.likedStatus = .cancel - album.likedCount = max((album.likedCount ?? 0) - 1, 0) - config(data: album) - - AIRoleProvider.request(.aiRolePhotoLikeOrNo(albumId: albumId, likedStatus: .cancel), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, albumId: albumId, isLike: false) - } - } else { - // 点赞 - likeView.playLotteLike { [weak self] completed in - guard let album = self?.data else { return } - album.likedStatus = .liked - album.likedCount = (album.likedCount ?? 0) + 1 - self?.config(data: album) - } - - AIRoleProvider.request(.aiRolePhotoLikeOrNo(albumId: albumId, likedStatus: .liked), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, albumId: albumId, isLike: true) - } - } - } - - // 统一处理请求结果 - private func handleLikeRequestResult(result: Result, albumId: Int, isLike: Bool) { - isRequesting = false - likeView.likeButton.isEnabled = true - - switch result { - case .success: - // 请求成功,无需额外处理 - break - case .failure: - // 请求失败,恢复状态 - if let currentId = self.data?.albumId, currentId == albumId { - guard let album = self.data else { return } - if isLike { - // 点赞失败,恢复为未点赞状态 - album.likedStatus = .cancel - album.likedCount = max((album.likedCount ?? 0) - 1, 0) - } else { - // 取消点赞失败,恢复为点赞状态 - album.likedStatus = .liked - album.likedCount = (album.likedCount ?? 0) + 1 - } - config(data: album) - } - } - } -} diff --git a/crush/Crush/Src/Modules/Role/Home/RoleHomePagerController.swift b/crush/Crush/Src/Modules/Role/Home/RoleHomePagerController.swift deleted file mode 100644 index 7c13616..0000000 --- a/crush/Crush/Src/Modules/Role/Home/RoleHomePagerController.swift +++ /dev/null @@ -1,446 +0,0 @@ -// -// RoleHomePagerController.swift -// Crush -// -// Created by Leon on 2025/7/24. -// - -import Combine -import UIKit -class RoleHomePagerController: CLViewController { - var navRightButton: EPIconGhostButton! - var likeView : HeartLikeCountView! - - var aiId: Int = 0 - - @Published var info: AIRoleInfo? - - /// 待修改的头像 - var avatarModel: UploadPhotoM? - - // 添加请求状态标记 - private var isRequesting = false - - private var cancellables = Set() - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDats() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - disabledFullScreenPan() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - enabledFullScreenPan() - } - - private func setupViews() { - navigationView.alpha0Title = "User Name" - navigationView.bgView.alpha = 0 - - - } - - private func setupDats() { - loadAIInfo(block: nil) - container.aboutVc.aiId = aiId - container.albumVc.aiId = aiId - } - - private func loadAIInfo(block: (() -> Void)?) { - Hud.showIndicator() - AIRoleProvider.request(.queryAIBaseInfo(aiId: aiId), modelType: AIRoleInfo.self) { [weak self] result in - Hud.hideIndicator() - switch result { - case let .success(success): - self?.info = success - case let .failure(failure): - switch failure { - case let .serviceError(code, _): - if code == .aiRoleNotExist{ - let alert = Alert(title: "The Role has been deleted", text: "The role has been deleted and cannot be accessed and interacted.") - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) {[weak self] in - self?.close() - } - alert.addAction(action1) - alert.show() - } - default: - break - } - } - block?() - } - } - - private func setupEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(notificationAIRoleInfoUpdated(noti:)), name: AppNotificationName.aiRoleInfoChanged.notificationName, object: nil) - container.createButton.addTarget(self, action: #selector(tapCreateButton), for: .touchUpInside) - container.headerView.avatarIv.tapAction = {[weak self] in - self?.tapEditAvatar() - } - - container.headerView.tapChatAction = { aiId in - AppRouter.goChatVC(aiId: aiId) - } - - container.headerView.tapEditAction = {[weak self] in - self?.tapEditAI() - } - - $info.sink { [weak self] result in - self?.navigationView.alpha0Title = result?.nickname ?? "" - self?.container.config(info: result) - self?.container.headerView.config(info: result) - self?.container.aboutVc.config(info: result) - - if let user = result{ - let uid = user.userId - self?.setupNaviRightViews(UserCore.shared.user?.userId == uid) - } - - if self?.likeView != nil{ - self?.likeView.isLike = result?.liked ?? false - } - }.store(in: &cancellables) - } - - // MARK: - Helper - private func setupNaviRightViews(_ isSelf: Bool = false){ -// if navRightButton != nil{ -// return -// } - - navigationView.rightStackH.removeSubviews() - - navigationView.paddingRightForRightStack = 16 - - likeView = { - let v = HeartLikeCountView(viewSize: .xl) - v.purIconStyle() - v.likeButton.addTarget(self, action: #selector(tapLikeButton), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - return v - }() - - if let user = info { - likeView.isLike = user.liked ?? false - } - - if isSelf{ - navRightButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .more) - v.addTarget(self, action: #selector(tapNaviMore(_:)), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - }else{ - navRightButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .shareBorder) - v.addTarget(self, action: #selector(tapShare), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - } - } - - // MARK: - Action - - @objc private func tapNaviMore(_ sender: UIButton) { -// let sheet = Sheet() -// let share = SheetAction(title: "Share", autoDismiss: true) { [weak self] in -// self?.tapShare() -// } -// let delete = SheetAction(title: "Delete", autoDismiss: true) { [weak self] in -// self?.alertDeleteRole() -// } -// sheet.addAction(share) -// sheet.addAction(delete) -// sheet.show() - - let pop = CLPopoverListView() - let rect = view.convert(sender.frame, from: sender.superview) - var items = [CLPopoverListTextItem]() - do { - let listItem = CLPopoverListTextItem() - listItem.title = "Share" - listItem.image = MWIconFont.image(fromIcon: .shareBorder, size: CGSize(width: 20, height: 20), color: .text) - listItem.updateLayout() - listItem.selectedHandler = {[weak self] _ in - self?.tapShare() - } - items.append(listItem) - } - do { - let listItem = CLPopoverListTextItem() - listItem.title = "Delete" - listItem.image = MWIconFont.image(fromIcon: .iconDelete, size: CGSize(width: 20, height: 20), color: .text) - listItem.updateLayout() - listItem.selectedHandler = {[weak self] _ in - self?.alertDeleteRole() - } - items.append(listItem) - } - - pop.setupCommonPopover(rect, inView: view, items: items, block: nil) - } - - @objc private func tapShare(){ - guard let aiId = info?.aiId else{return} - let content = "Come to Crushlevel for chat, Crush, and AI - chat." - let urlString = "\(AppConst.h5urlRoot)/@\(aiId)" - - guard let url = URL(string: urlString) else { return } - - // 分享内容(文字 + 链接) - let items: [Any] = [url, content] - - let activityVC = UIActivityViewController(activityItems: items, - applicationActivities: nil) - - // iPad 需要设置弹出位置,否则会崩溃 - if let popover = activityVC.popoverPresentationController { - popover.sourceView = view - popover.sourceRect = CGRect(x: view.bounds.midX, - y: view.bounds.midY, - width: 0, height: 0) - popover.permittedArrowDirections = [] - } - - present(activityVC, animated: true, completion: nil) - } - - @objc private func tapLikeButton(){ - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} - - // 防止重复点击 - guard !isRequesting else { return } - guard let user = info, let aiId = user.aiId else { return } - - let isliked = user.liked.boolValue - let likedCount = user.likedNum ?? 0 - - isRequesting = true - - // 禁用按钮 - likeView.likeButton.isEnabled = false - - if isliked { - // 取消点赞 - let likeNewStatus = LikeOrCancelStatus.cancel - let newLikedCount = max(likedCount - 1, 0) - - // 立即更新UI状态 - info?.liked = false - info?.likedNum = newLikedCount - likeView.isLike = false - likeView.countLabel.text = String.displayNumber(NSNumber(value: newLikedCount), scale: 1) - - AIRoleProvider.request(.aiUserLikeOrCancel(aiId: aiId, likedStatus: likeNewStatus), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, aiId: aiId, isLike: false, originalLikedCount: likedCount) - } - } else { - // 点赞 - let likeNewStatus = LikeOrCancelStatus.liked - let newLikedCount = likedCount + 1 - - // 播放点赞动画 - likeView.playLotteLike { [weak self] completed in - // 立即更新UI状态 - self?.info?.liked = true - self?.info?.likedNum = newLikedCount - self?.likeView.isLike = true - self?.likeView.countLabel.text = String.displayNumber(NSNumber(value: newLikedCount), scale: 1) - } - - AIRoleProvider.request(.aiUserLikeOrCancel(aiId: aiId, likedStatus: likeNewStatus), modelType: EmptyModel.self) { [weak self] result in - self?.handleLikeRequestResult(result: result, aiId: aiId, isLike: true, originalLikedCount: likedCount) - } - } - } - - // 统一处理请求结果 - private func handleLikeRequestResult(result: Result, aiId: Int, isLike: Bool, originalLikedCount: Int) { - isRequesting = false - likeView.likeButton.isEnabled = true - - switch result { - case .success: - // 请求成功,无需额外处理 - break - case .failure: - // 请求失败,恢复状态 - if let currentAiId = self.info?.aiId, currentAiId == aiId { - if isLike { - // 点赞失败,恢复为未点赞状态 - info?.liked = false - info?.likedNum = originalLikedCount - likeView.isLike = false - likeView.countLabel.text = String.displayNumber(NSNumber(value: originalLikedCount), scale: 1) - } else { - // 取消点赞失败,恢复为点赞状态 - info?.liked = true - info?.likedNum = originalLikedCount - likeView.isLike = true - likeView.countLabel.text = String.displayNumber(NSNumber(value: originalLikedCount), scale: 1) - } - } - } - // 刷新统计 - container.aboutVc.loadStatistics() - } - - private func alertDeleteRole() { - let alert = Alert(title: "Delete Role", text: "删除角色不可恢复。为保障用户体验,角色删除后,已经与该角色发生过聊天的或者付费行为的用户,还可以正常与该角色互动。") - let delete = AlertAction(title: "Delete", actionStyle: .destructive) { [weak self] in - self?.doDeleteRole() - } - let cancel = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(delete) - alert.addAction(cancel) - alert.show() - } - - @objc private func tapCreateButton() { -// let sheet = Sheet() -// sheet.title = "Creation Option" -// let option1 = SheetAction(title: "Static Image", autoDismiss: true) {[weak self] in -// self?.goGenerateMoreAlbums() -// } -// let option2 = SheetAction(title: "Dynamic Image", autoDismiss: true) { -// Hud.comingsoon() -// } -// sheet.addAction(option1) -// sheet.addAction(option2) -// sheet.show() - - goGenerateMoreAlbums() - } - - @objc private func tapEditAI(){ - // Convert AIRoleInfo to - AppRouter.goCreateEditAIRole(aiId: aiId) - } - - @objc private func tapEditAvatar(){ - guard UserCore.shared.isSelf(id: info?.userId) else{ - return - } - - guard let imgUrl = info?.homeImageUrl else {return} - - ImageDownloader.downloadImage(from: imgUrl) {[weak self] img in - guard let image = img else{return} - self?.cropAIAvatar(image: image) - } - - } - - - // MARK: - Functions - - private func goGenerateMoreAlbums(){ - guard let aiId = info?.aiId, info != nil else {return} - - let vc = RolePhotoGenerateController(type: .album) - vc.aiId = aiId - vc.introduction = info?.introduction - vc.sex = info?.sex - vc.birthday = info?.birthday - presentNaviRootVc(vc: vc) - } - - private func doDeleteRole() { - guard let aiInfo = info else { return } - - Hud.showIndicator() - AIRoleProvider.request(.aiDel(id: aiInfo.aiId!)) { [weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.alertDeleteOK() - NotificationCenter.post(name: .aiRoleCreatedOrDelete) - UserCore.shared.refreshUserInfo(block: nil) - case let .failure(failure): - dlog(failure) - } - } - } - - private func alertDeleteOK() { - let alert = Alert(title: "The Role has been deleted", text: "The role has been deleted and cannot be accessed and interacted") - let gotit = AlertAction(title: "OK", actionStyle: .confirm) { [weak self] in - self?.navigationController?.popViewController(animated: true) - } - alert.addAction(gotit) - alert.show() - } - - @objc private func notificationAIRoleInfoUpdated(noti: Notification) { - loadAIInfo(block: nil) - } - - private func cropAIAvatar(image:UIImage?){ - guard let avatar = image else {return} - - let image = avatar - -// let image = UIImage(named: "egpic")! - let cropViewController = CropViewController(image: image) { [unowned self] croppedImage in - self.container.headerView.avatarIv.bindImage(img: croppedImage) - - // 裁剪后的图片 - let photo = UploadPhotoM() - photo.image = croppedImage - photo.addThisItemTimeStamp = Date().timeStamp - avatarModel = photo - - Hud.showIndicator() - CloudStorage.shared.s3BatchAddPhotos([photo], bucket: .ROLE) {[weak self] result in - - if result{ - //self?.doPublishRole() - self?.doUpdateRoleAvatar() - }else{ - Hud.hideIndicator() - } - } - } - let navc = CLNavigationController(rootViewController: cropViewController) - UIWindow.getTopViewController()?.present(navc, animated: true, completion: nil) - - } - - private func doUpdateRoleAvatar(){ - guard let photo = avatarModel, let remoteFullPath = photo.remoteFullPath else{ - Hud.hideIndicator() - return - } - - AIRoleProvider.request(.modifyAIAvatar(aiId: aiId, userHead: remoteFullPath), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.info?.headImg = remoteFullPath - self?.container.headerView.config(info: self?.info) - self?.avatarModel = nil - case .failure: - break - } - } - - - } -} diff --git a/crush/Crush/Src/Modules/Role/Home/View/HeartLikeCountView.swift b/crush/Crush/Src/Modules/Role/Home/View/HeartLikeCountView.swift deleted file mode 100644 index db377e1..0000000 --- a/crush/Crush/Src/Modules/Role/Home/View/HeartLikeCountView.swift +++ /dev/null @@ -1,257 +0,0 @@ -// -// HeartLikeCountView.swift -// Crush -// -// Created by Leon on 2025/7/25. -// - -import UIKit -import SnapKit // Assuming SnapKit is used for layout instead of Masonry -import Lottie // Assuming Lottie is used for animations - -enum HeartLikeCountSize { - case small - // h: 用在Album - case medium - case large - // wh: 44x44 - case xl - // wh: 48x48 - case xxl -} - -/// 默认创建: Medium -class HeartLikeCountView: UIView { - - // MARK: - Properties - - let countLabel: UILabel - let countImageView: UIImageView - let likeButton: UIButton - let lotiView: LottieAnimationView - let contentStackView: UIStackView - - var viewSize: HeartLikeCountSize = .medium - - var isLike: Bool = false { - didSet { - let code = isLike ? IconCode.likeFill : IconCode.like - let color: UIColor = isLike ? UIColor.c.cin : UIColor.white - - // 根据尺寸配置图标大小 - let iconSize = getIconSize() - let image = MWIconFont.image(fromIcon: code, size: iconSize, color: color) - countImageView.image = image - } - } - - // MARK: - Initialization - - init(viewSize: HeartLikeCountSize = .medium) { - self.viewSize = viewSize - countLabel = UILabel() - countImageView = UIImageView() - likeButton = UIButton(type: .custom) - lotiView = LottieAnimationView(name: "like_motion") - lotiView.contentMode = .scaleAspectFit - contentStackView = UIStackView() - - super.init(frame: .zero) - - setupUI() - setupTheme() - setupSize() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Setup Methods - - private func setupTheme() { - countLabel.textColor = .c.ctpn - } - - private func setupUI() { - backgroundColor = .c.csedn - - // Setup contentStackView - addSubview(contentStackView) - contentStackView.axis = .horizontal - contentStackView.alignment = .center - contentStackView.distribution = .fill - - // Setup countImageView - countImageView.contentMode = .scaleAspectFill - contentStackView.addArrangedSubview(countImageView) - - // Setup countLabel - contentStackView.addArrangedSubview(countLabel) - - // Setup lotiView - addSubview(lotiView) - lotiView.isHidden = true - - // Setup likeButton - addSubview(likeButton) - likeButton.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - isLike = false - } - - private func setupSize() { - let sizeConfig = getSizeConfig() - - self.snp.makeConstraints { make in - make.height.equalTo(sizeConfig.cornerRadius * 2) - } - // 设置整体圆角 - layer.cornerRadius = sizeConfig.cornerRadius - - // 设置 contentStackView 约束 - contentStackView.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(sizeConfig.horizontalPadding) - make.trailing.equalToSuperview().offset(-sizeConfig.horizontalPadding) - make.centerY.equalToSuperview() - } - - // 设置图标大小 - countImageView.snp.makeConstraints { make in - make.size.equalTo(sizeConfig.iconSize) - } - - // 设置 StackView 间距 - contentStackView.spacing = sizeConfig.iconTextSpacing - - // 设置字体 - countLabel.font = sizeConfig.font - - // 设置 Lottie 动画视图约束 - lotiView.snp.makeConstraints { make in - make.centerX.equalTo(countImageView) - make.centerY.equalTo(countImageView) - make.size.equalTo(sizeConfig.lottieSize) - } - } - - // MARK: - Size Configuration - - private struct SizeConfig { - let iconSize: CGSize - let font: UIFont - let cornerRadius: CGFloat - let horizontalPadding: CGFloat - let iconTextSpacing: CGFloat - let lottieSize: CGSize - } - - private func getSizeConfig() -> SizeConfig { - switch viewSize { - case .small: - return SizeConfig( - iconSize: CGSize(width: 12, height: 12), - font: .t.tls, - cornerRadius: 12, - horizontalPadding: 8, - iconTextSpacing: 3, - lottieSize: CGSize(width: 24, height: 24) - ) - case .medium: - return SizeConfig( - iconSize: CGSize(width: 16, height: 16), - font: .t.tlm, - cornerRadius: 16, - horizontalPadding: 12, - iconTextSpacing: 4, - lottieSize: CGSize(width: 30, height: 30) - ) - case .large: - return SizeConfig( - iconSize: CGSize(width: 20, height: 20), - font: .t.tll, - cornerRadius: 20, - horizontalPadding: 16, - iconTextSpacing: 6, - lottieSize: CGSize(width: 36, height: 36) - ) - case .xl: - return SizeConfig( - iconSize: CGSize(width: 20, height: 20), - font: .t.tll, - cornerRadius: 20, - horizontalPadding: 12, - iconTextSpacing: 8, - lottieSize: CGSize(width: 44, height: 44) - ) - case .xxl: - return SizeConfig( - iconSize: CGSize(width: 24, height: 24), - font: .t.tll, - cornerRadius: 24, - horizontalPadding: 32, - iconTextSpacing: 8, - lottieSize: CGSize(width: 48, height: 48) - ) - - } - } - - private func getIconSize() -> CGSize { - return getSizeConfig().iconSize - } - - // MARK: - Lazy Properties - - private func configureCountLabel() -> UILabel { - let label = UILabel() - let typography = CLSystemToken.typography(token: .tnms) - label.font = typography.font - return label - } - - private func configureCountImageView() -> UIImageView { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - return imageView - } - - private func configureLikeButton() -> UIButton { - return UIButton(type: .custom) - } - - private func configureLotiView() -> LottieAnimationView { - let view = LottieAnimationView(name: "like_motion") - view.isHidden = true - return view - } - - // MARK: - Public - - func purIconStyle(){ - backgroundColor = .clear - countLabel.isHidden = true - } - - func playLotteLike(_ completion:((Bool)->Void)?){ - lotiView.isHidden = false - countImageView.alpha = 0 // 使用 alpha 而不是 isHidden - lotiView.play {[weak self] completed in - self?.lotiView.isHidden = true - self?.countImageView.alpha = 1 // 恢复显示 - completion?(completed) - } - } - - func bind(aiAlbum: AlbumPhotoItem?){ - guard let photo = aiAlbum else{return} - - isLike = photo.likedStatus == .liked - let likedCount = photo.likedCount ?? 0 - let likeCountDisplay = String.displayNumber(NSNumber(value: likedCount), scale: 1) - countLabel.text = likeCountDisplay - } -} diff --git a/crush/Crush/Src/Modules/Role/Home/View/RoleHomeAboutView.swift b/crush/Crush/Src/Modules/Role/Home/View/RoleHomeAboutView.swift deleted file mode 100644 index 96a9270..0000000 --- a/crush/Crush/Src/Modules/Role/Home/View/RoleHomeAboutView.swift +++ /dev/null @@ -1,568 +0,0 @@ -// -// RoleHomeAboutView.swift -// Crush -// -// Created by Leon on 2025/7/24. -// - -import UIKit - -let giftCountOnePage = 12 -let giftItemWidth = floor((UIScreen.width - 40 * 2 - 12 * 3) / 4.0) as CGFloat -let giftItemHeight: CGFloat = giftItemWidth + 24 - -class RoleHomeAboutView: CLContainer { - var container: LTScrollContainer! - - // Data panel - var likePanelItem: HomeAboutDataPanelItem! - var chatPanelItem: HomeAboutDataPanelItem! - var peoplePanelItem: HomeAboutDataPanelItem! - var giftContainer: UIView! - var giftPanelItem: HomeAboutDataPanelItem! - - // Introduction - var introductionLabel: LineSpaceLabel! - var expendButton: EPIconGhostButton! - - // Gifts - 修改为外层横向滚动 - var outerCollectionView: UICollectionView! - var pageControl: CLPageControl! - - // 礼物数据 - var giftsData: [AIRoleGiftReceivedList] = [] - var currentPage: Int = 0 - - var info: AIRoleInfo? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - container = { - let v = LTScrollContainer() - v.stack.spacing = 16 - v.stackEdge = UIEdgeInsets(top: 12, left: 0, bottom: 16, right: 0) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - backgroundColor = .random - - setupCommonDataPanel() - setupIntroduction() - setupGiftsView() - } - - private func setupCommonDataPanel() { - let block = UIView() - block.backgroundColor = .c.csbn - block.layer.cornerRadius = 16 - block.layer.masksToBounds = true - container.stack.addArrangedSubview(block) - block.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.height.equalTo(80) - } - - let stackH = { - let v = UIStackView() - v.spacing = 0 - v.distribution = .fillEqually - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - return v - }() - - likePanelItem = { - let v = HomeAboutDataPanelItem() - v.countLabel.text = "0" - v.tipLabel.text = "Liked" - stackH.addArrangedSubview(v) - return v - }() - - chatPanelItem = { - let v = HomeAboutDataPanelItem() - v.countLabel.text = "0" - v.tipLabel.text = "Chats" - stackH.addArrangedSubview(v) - return v - }() - - peoplePanelItem = { - let v = HomeAboutDataPanelItem() - v.countLabel.text = "0" - v.tipLabel.text = "People" - stackH.addArrangedSubview(v) - return v - }() - - giftPanelItem = { - let v = HomeAboutDataPanelItem() - v.countLabel.text = "0" - #warning("translate") - v.tipLabel.text = "CrushCoin" - stackH.addArrangedSubview(v) - return v - }() - } - - private func setupIntroduction() { - let block = UIView() - block.backgroundColor = .c.csbn - block.layer.cornerRadius = 16 - block.layer.masksToBounds = true - container.stack.addArrangedSubview(block) - block.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - // make.height.equalTo(80) - } - - let titleLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tts - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - } - return v - }() - - let stackH = { - let v = UIStackView() - v.spacing = 0 - v.alignment = .bottom - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(titleLabel.snp.bottom).offset(8) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - introductionLabel = { - let v = LineSpaceLabel() - let typography = CLSystemToken.typography(token: .tbm) - v.textColor = .text - v.config(typography) - v.numberOfLines = 3 - stackH.addArrangedSubview(v) - return v - }() - - expendButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .xs, iconCode: .iconFullimage) - v.addTarget(self, action: #selector(tapExpandButton), for: .touchUpInside) - stackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - titleLabel.text = "Introduction" -// #warning("test") -// introductionLabel.text = "She is a new and beautiful teacher and has just graduated. You are the most rebellious student of the whole school with some sth.She is a new and beautiful teacher and has just graduated. You are the most rebellious student of the whole school with some." - } - - private func setupGiftsView() { - let block = UIView() - block.backgroundColor = .c.csbn - block.layer.cornerRadius = 16 - block.layer.masksToBounds = true - container.stack.addArrangedSubview(block) - block.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - self.giftContainer = block - - let titleLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tts - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - } - return v - }() - titleLabel.text = "Gifts" - - // 外层横向滚动的CollectionView - let outerLayout = UICollectionViewFlowLayout() - let widthOfOut = UIScreen.width - 24*2 - outerLayout.itemSize = CGSize(width: widthOfOut, height: 200) // 初始高度,后续会动态调整 - outerLayout.scrollDirection = .horizontal - outerLayout.minimumLineSpacing = 0 - outerLayout.minimumInteritemSpacing = 0 - - outerCollectionView = { - let v = UICollectionView(frame: .zero, collectionViewLayout: outerLayout) - v.backgroundColor = .c.csbn - v.showsHorizontalScrollIndicator = false - v.isPagingEnabled = true - v.register(GiftPageCell.self, forCellWithReuseIdentifier: "GiftPageCell") - v.dataSource = self - v.delegate = self - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(12) - make.height.equalTo(160) // 初始高度,后续会动态调整 - } - return v - }() - - // PageControl - pageControl = { - let v = CLPageControl() - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(outerCollectionView.snp.bottom).offset(8) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - } - - // MARK: - Public - - func config(info: AIRoleInfo?) { - self.info = info - guard let data = info else { return } - - introductionLabel.text = data.introduction - - if UserCore.shared.isSelf(id: info?.userId){ - giftPanelItem.isHidden = false - }else{ - giftPanelItem.isHidden = true - } - } - - func configStat(data: AIRoleStatisticsResponse?) { - guard let stat = data else { return } -// likePanelItem.countLabel.text = String.thousandString(stat.likedNum ?? 0) -// chatPanelItem.countLabel.text = String.thousandString(stat.chatNum ?? 0) -// peoplePanelItem.countLabel.text = String.thousandString(stat.conversationNum ?? 0) -// giftPanelItem.countLabel.text = String.thousandString(float: (Double(stat.coinNum ?? 0)*0.01)) - - likePanelItem.countLabel.text = String.displayNumber(NSNumber(value: stat.likedNum ?? 0), scale: 1) - chatPanelItem.countLabel.text = String.displayNumber(NSNumber(value: stat.chatNum ?? 0), scale: 1) //String.thousandString(stat.chatNum ?? 0) - peoplePanelItem.countLabel.text = String.displayNumber(NSNumber(value: stat.conversationNum ?? 0), scale: 1)//String.thousandString(stat.conversationNum ?? 0) - giftPanelItem.countLabel.text = String.displayNumber(NSNumber(value: (Double(stat.coinNum ?? 0)*0.01)), scale: 1)//String.thousandString(float: (Double(stat.coinNum ?? 0)*0.01)) - } - - func configGifts(data: [AIRoleGiftReceivedList]?) { - //guard let gifts = data else { return } - - giftsData = data ?? [] - -// #warning("test") -// giftsData = []//Array(gifts.prefix(3)) - - if giftsData.count > 0{ - outerCollectionView.isHidden = false - pageControl.isHidden = false - giftContainer.hideEmpty() - }else{ - giftContainer.showStartYEmpty(text: "No gift yet", startY: 52) - outerCollectionView.isHidden = true - pageControl.isHidden = true - return - } - - // 计算总页数(每页最多12个礼物) - let totalPages = max(1, (giftsData.count) / giftCountOnePage) - - // 动态计算高度 - let giftHeight: CGFloat = giftItemHeight - let rowSpacing: CGFloat = 12.0 - var rows = 1.0 - if giftsData.count > 8{ - rows = 3.0 - }else if giftsData.count > 4{ - rows = 2.0 - } - let calculatedHeight = giftHeight * rows + rowSpacing*(rows-1) - - // 更新outerCollectionView的高度约束 - outerCollectionView.snp.updateConstraints { make in - make.height.equalTo(calculatedHeight) - } - - // 更新layout的itemSize - if let layout = outerCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { - let widthOfOut = UIScreen.width - 24*2 - layout.itemSize = CGSize(width: widthOfOut, height: calculatedHeight) - } - - // 根据总页数决定是否显示pageControl - if totalPages > 1 { - pageControl.isHidden = false - pageControl.numberOfPages = totalPages - pageControl.currentPage = 0 - } else { - pageControl.isHidden = true - } - - // 重新加载外层CollectionView - outerCollectionView.reloadData() - } - - // MARK: - Action - - @objc private func tapExpandButton() { -// introductionLabel.numberOfLines = 0 -// expendButton.isHidden = true -// layoutIfNeeded() - - let vc = RoleIntroBrowseDialogController() - vc.introduction = info?.introduction - vc.imageUrl = info?.homeImageUrl - vc.show() - } - - class HomeAboutDataPanelItem: UIView { - var countLabel: UILabel! - var tipLabel: UILabel! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - countLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tnml - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - - tipLabel = { - let v = UILabel() - v.textColor = .desc - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(countLabel.snp.bottom).offset(4) - make.bottom.equalToSuperview() - } - return v - }() - } - } -} - -class HomeAboutDataGiftGridCell: UICollectionViewCell { - var block: UIView! - var imageView: UIImageView! - var countLabel: UILabel! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - // block, full width, height 88 - block = { - let v = UIView() - v.backgroundColor = .c.csnn - v.layer.cornerRadius = 8 - v.layer.masksToBounds = true - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalToSuperview() - make.height.equalTo(64) - } - return v - }() - - imageView = { - let v = UIImageView() - v.contentMode = .scaleAspectFill - v.layer.cornerRadius = 8 - v.layer.masksToBounds = true - v.image = UIImage(named: "icon_32_diamond") - block.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 32, height: 32)) - make.center.equalToSuperview() - } - return v - }() - - countLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.tls - v.text = "x 100" - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(block.snp.bottom).offset(4) - // make.bottom.equalToSuperview() - } - return v - }() - } - - func config(_ data: AIRoleGiftReceivedList?){ - guard let gift = data else{return} - - countLabel.text = "x \(gift.getNum ?? 0)" - // 设置礼物图片 - if let iconUrl = gift.icon, !iconUrl.isEmpty { - imageView.loadImage(gift.icon, bgColor: nil) - } - } -} - -// 新增:礼物页面Cell -class GiftPageCell: UICollectionViewCell { - var innerCollectionView: UICollectionView! - var giftsData: [AIRoleGiftReceivedList] = [] - - - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - let itemWidth = giftItemWidth//floor((UIScreen.width - 40 * 2 - 12 * 3) / 4.0) as CGFloat - let itemHeight = giftItemHeight //CGFloat = itemWidth + 24 - layout.itemSize = .init(width: itemWidth, height: itemHeight) - layout.minimumLineSpacing = 12 - layout.minimumInteritemSpacing = 12 - layout.sectionInset = .init(top: 0, left: 16, bottom: 0, right: 16) - - innerCollectionView = { - let v = UICollectionView(frame: .zero, collectionViewLayout: layout) - v.backgroundColor = .clear - v.showsHorizontalScrollIndicator = false - v.isScrollEnabled = false - v.register(HomeAboutDataGiftGridCell.self, forCellWithReuseIdentifier: "HomeAboutDataGiftGridCell") - v.dataSource = self - v.delegate = self - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - } - - func config(gifts: [AIRoleGiftReceivedList], pageIndex: Int) { - self.giftsData = gifts - innerCollectionView.reloadData() - } -} - -extension GiftPageCell: UICollectionViewDataSource, UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return giftsData.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeAboutDataGiftGridCell", for: indexPath) as! HomeAboutDataGiftGridCell - - let gift = giftsData[indexPath.item] - cell.config(gift) - - return cell - } -} - -extension RoleHomeAboutView: UICollectionViewDataSource, UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if collectionView == outerCollectionView { - // 外层CollectionView:每页giftCountOnePage个礼物 - return max(1, (giftsData.count) / giftCountOnePage) - } - return 0 - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - if collectionView == outerCollectionView { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GiftPageCell", for: indexPath) as! GiftPageCell - - // 计算当前页的礼物数据 - let startIndex = indexPath.item * giftCountOnePage - let endIndex = min(Int(startIndex + giftCountOnePage), giftsData.count) - let pageGifts = Array(giftsData[startIndex.. CGSize { - if collectionView == outerCollectionView { - // 每行4个, 最多3行,根据giftsData.count计算高度, 每行间隔12 - let row = ceil(Double(giftsData.count) / 4.0) - let height = giftItemHeight * CGFloat(row) + 12 * CGFloat(row - 1) - return CGSize(width: UIScreen.width - 40 * 2, height: height) - } - return CGSize.zero - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if scrollView == outerCollectionView { - let pageWidth = scrollView.frame.width - let currentPage = Int(round(scrollView.contentOffset.x / pageWidth)) - if currentPage != self.currentPage { - self.currentPage = currentPage - pageControl.currentPage = currentPage - } - } - } -} diff --git a/crush/Crush/Src/Modules/Role/Home/View/RoleHomePagerHeader.swift b/crush/Crush/Src/Modules/Role/Home/View/RoleHomePagerHeader.swift deleted file mode 100644 index 6721c9b..0000000 --- a/crush/Crush/Src/Modules/Role/Home/View/RoleHomePagerHeader.swift +++ /dev/null @@ -1,275 +0,0 @@ -// -// RoleHomePagerHeader.swift -// Crush -// -// Created by Leon on 2025/7/24. -// - -import UIKit - -class RoleHomePagerHeader: UIView { - var bgIvContainer: UIView! - var bgIv: UIImageView! - var overlay: GradientView! - - var userBasicInfoView: UIView! - var avatarIv: AvatarView! - var buttonStackH : UIStackView! - private var iconButton : EPIconTertiaryButton! - private var editButton: StyleButton! - var nameLabel: UILabel! - - // tags - var additionView: UIStackView! - var ageLabel: CLIconLabel! - var roleTags: [EPTagLabel] = [] - - var heightChangeBlock: ((CGFloat) -> Void)? - var tapEditAction: (() -> Void)? - var tapChatAction: ((_ aiId:Int) -> Void)? - - // Data - var info:AIRoleInfo? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bgIvContainer = { - let v = UIView() - v.clipsToBounds = true - addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalTo(264 + UIWindow.navBarHeight) - } - return v - }() - - bgIv = { - let v = UIImageView() - v.clipsToBounds = true - bgIvContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.height.equalToSuperview().priority(.low) - } - return v - }() - - overlay = { - let gradient = CLSystemToken.gradient(token: .cob) - let v = GradientView(colors: [gradient.firstColor!, gradient.secondColor!], gradientType: .topToBottom) - bgIvContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - make.height.equalTo(160) - } - return v - }() - - do { - userBasicInfoView = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(bgIvContainer.snp.bottom) - make.height.equalTo(64) - } - return v - }() - - avatarIv = { - let v = AvatarView() - userBasicInfoView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.centerY.equalToSuperview() - make.size.equalTo(CGSize(width: 64, height: 64)) - } - return v - }() - - buttonStackH = { - let v = UIStackView() - v.spacing = 12 - v.alignment = .center - userBasicInfoView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview() - } - return v - }() - - iconButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .chat) - v.addTarget(self, action: #selector(tapChatButton), for: .touchUpInside) - buttonStackH.addArrangedSubview(v) - v.isHidden = true - return v - }() - - editButton = { - let v = StyleButton() - v.addTarget(self, action: #selector(tapOperateButton), for: .touchUpInside) - v.tertiary(size: .small) - buttonStackH.addArrangedSubview(v) - return v - }() - - nameLabel = { - let v = UILabel() - v.textColor = .text - v.font = .t.ttm - v.setContentCompressionResistancePriority(UILayoutPriority(744), for: .horizontal) - userBasicInfoView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(avatarIv.snp.trailing).offset(12) - //make.trailing.equalTo(buttonStackH.snp.leading).offset(-12) - make.trailing.lessThanOrEqualTo(buttonStackH.snp.leading).offset(-12) - } - return v - }() - } - - do { - additionView = { - let v = UIStackView() - v.spacing = 8 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.lessThanOrEqualToSuperview().offset(-24) - make.top.equalTo(userBasicInfoView.snp.bottom).offset(12) - } - return v - }() - - ageLabel = { - let v = CLIconLabel() - v.iconImageView.image = UIImage(named: "sex_female_flag") - v.innerLRPadding = 4 - v.contentLabel.text = "-" - v.backgroundColor = .c.csen - v.layer.cornerRadius = 4; - v.layer.masksToBounds = true - return v - }() - } - - editButton.setTitle("Edit", for: .normal) - -// #warning("test") -// testData() - } - - private func testData() { - bgIv.image = UIImage(named: "egpic") - bgIv.snp.makeConstraints { make in - // w:h=1024:1820 - make.width.equalTo(bgIv.snp.height).multipliedBy(1024.0 / 1820.0) - } - // avatarIv.image = UIImage(named: "egpic") - nameLabel.text = "Angelique" - - do { - let roleTag = EPTagLabel(style: .default, size: .small, text: "Sensibility") - additionView.addArrangedSubview(roleTag) - } - } - - func config(info:AIRoleInfo?){ - self.info = info - guard let data = info else {return} - - bgIv.loadImage(data.homeImageUrl, completionBlock: {[weak self] result in - switch result { - case .success(let success): - let image = success.image - self?.bgIv.snp.makeConstraints { make in - // w:h=1024:1820 - make.width.equalTo((self?.bgIv.snp.height)!).multipliedBy(image.size.width / image.size.height) - } - case .failure(let failure): - dlog(failure) - } - }) - - if UserCore.shared.isSelf(id: data.userId){ - editButton.setTitle("Edit", for: .normal) - editButton.tertiary(size: .small) - iconButton.isHidden = false - }else{ - editButton.setTitle("Chat", for: .normal) - editButton.primary(size: .small) - iconButton.isHidden = true - } - - avatarIv.bindImageUrl(imgUrl: data.headImg) - - nameLabel.text = data.nickname - - // Reset - additionView.removeSubviews() - - // Age - let sex: Sex = data.sex ?? .noncomfirming - ageLabel.iconImageView.image = sex.icon - additionView.addArrangedSubview(ageLabel) - - if let birthday = data.birthday{ - let date = Date.dateFromMilliseconds(Int64(birthday)) - let age = Date().years(from: date) - ageLabel.contentLabel.text = "\(age)" - } - -// if !String.realEmpty(str: data.roleName){ -// let roleTag = EPTagLabel(style: .default, size: .small, text: data.roleName!) -// additionView.addArrangedSubview(roleTag) -// } - - if !String.realEmpty(str: data.characterName){ - let roleTag = EPTagLabel(style: .default, size: .small, text: data.characterName!) - additionView.addArrangedSubview(roleTag) - } - - if !String.realEmpty(str: data.tagName){ - let roleTag = EPTagLabel(style: .default, size: .small, text: data.tagName!) - additionView.addArrangedSubview(roleTag) - } - } - - @objc private func tapOperateButton(){ - guard let data = info, let aiId = data.aiId else {return} - if UserCore.shared.isSelf(id: data.userId){ - tapEditAction?() - }else{ - tapChatAction?(aiId) - } - - } - - @objc private func tapChatButton(){ - guard let data = info, let aiId = data.aiId else {return} - tapChatAction?(aiId) - } - - // MARK: - Other - - override func layoutSubviews() { - super.layoutSubviews() - let maxY = additionView.frame.maxY + 16 - heightChangeBlock?(maxY) - } -} diff --git a/crush/Crush/Src/Modules/Role/Home/View/RoleHomePagerView.swift b/crush/Crush/Src/Modules/Role/Home/View/RoleHomePagerView.swift deleted file mode 100644 index c0ed517..0000000 --- a/crush/Crush/Src/Modules/Role/Home/View/RoleHomePagerView.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// RoleHomePagerView.swift -// Crush -// -// Created by Leon on 2025/7/24. -// - -import JXPagingView -import JXSegmentedView -import UIKit - -class RoleHomePagerView: CLContainer { - - var tapCreateAction: (()-> Void)? - - private let segmentedViewHeight = 40 - private lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - private lazy var pagingView = JXPagingListRefreshView(delegate: self) - private var controllers = [JXPagingViewListViewDelegate]() - private let dataSource = JXSegmentedTitleDataSource() - - var headerView: RoleHomePagerHeader! - private var headerViewHeight: CGFloat = 288 - lazy var aboutVc: RoleHomeAboutController = RoleHomeAboutController() - lazy var albumVc: RoleHomeAlbumController = RoleHomeAlbumController() - - var createButton: StyleButton! - - var roleInfo: AIRoleInfo? - - override init(frame: CGRect) { - super.init(frame: frame) - - setupViews() - setupDats() - setupEvents() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - headerView = { - let v = RoleHomePagerHeader() - v.heightChangeBlock = { [weak self] height in - self?.headerViewHeight = height - self?.pagingView.resizeTableHeaderViewHeight() - } - return v - }() - - pagingView.mainTableView.backgroundColor = UIColor.clear - pagingView.pinSectionHeaderVerticalOffset = Int(UIWindow.navBarTotalHeight) - pagingView.mainTableView.gestureDelegate = self - addSubview(pagingView) - pagingView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - make.top.equalToSuperview() - } - - dataSource.clNormalStyle() - dataSource.titles = ["About", "Album"] - - segmentedView.listContainer = pagingView.listContainerView - segmentedView.dataSource = dataSource - segmentedView.delegate = self - segmentedView.clNormalStyle() - - controllers.append(aboutVc) - controllers.append(albumVc) - - createButton = { - let v = StyleButton() - v.defaultSecondary(size: .small) - - segmentedView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-24) - } - v.setTitle("Create", for: .normal) - v.isHidden = true - return v - }() - } - - private func setupDats() { - } - - private func setupEvents() { - headerView.heightChangeBlock = { [weak self] height in - self?.headerViewHeight = height - self?.pagingView.resizeTableHeaderViewHeight() - } - } - - func config(info: AIRoleInfo?) { - roleInfo = info - guard let data = info, let userId = data.userId, let aiId = data.aiId else { return } - - albumVc.aiId = aiId - albumVc.userId = userId - - refreshCreateButtonHidden() - } - - // MARK: - Action - - - // MARK: - Helper - - private func refreshCreateButtonHidden(){ - guard let info = roleInfo else{return} - if UserCore.shared.isSelfByUid(uid: info.userId) && segmentedView.selectedIndex == 1{ - createButton.isHidden = false - }else{ - createButton.isHidden = true - } - } -} - -extension RoleHomePagerView: JXPagingViewDelegate, JXSegmentedViewDelegate { - func tableHeaderViewHeight(in _: JXPagingView) -> Int { - return Int(headerViewHeight) - } - - func tableHeaderView(in _: JXPagingView) -> UIView { - return headerView - } - - func heightForPinSectionHeader(in _: JXPagingView) -> Int { - return segmentedViewHeight - } - - func viewForPinSectionHeader(in _: JXPagingView) -> UIView { - return segmentedView - } - - func numberOfLists(in _: JXPagingView) -> Int { - return controllers.count - } - - func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { - let vc = controllers[index] - return vc - } - - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { - refreshCreateButtonHidden() - } - - func mainTableViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviViewsAlpha(scrollView: scrollView, alphaViews: [navigationView?.titleLabel, navigationView?.bgView], oppositeViews: []) - } -} - -extension RoleHomePagerView: JXPagingMainTableViewGestureDelegate { - func mainTableViewGestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool - { - // 如果是 UICollectionView 的手势,先判断滚动方向 - if let panGesture = otherGestureRecognizer as? UIPanGestureRecognizer, - let otherView = otherGestureRecognizer.view, - otherView is UICollectionView { - - let velocity = panGesture.velocity(in: otherView) - // 横向滚动时禁止 - if abs(velocity.x) > abs(velocity.y) { - return false - } - } - // 禁止segmentedView左右滑动的时候,上下和左右都可以滚动 - if otherGestureRecognizer == segmentedView.collectionView.panGestureRecognizer { - return false - } - return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoGenerateController.swift b/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoGenerateController.swift deleted file mode 100644 index 8dc3efa..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoGenerateController.swift +++ /dev/null @@ -1,364 +0,0 @@ -// -// RolePhotoGenerateController.swift -// Crush -// -// Created by Leon on 2025/7/27. -// - -import UIKit - -/// 二创类型 -enum RolePhotoGenerateType { - case album // 相册创建 - case figure // 形象编辑 -} - -/// 和RoleFigureGenerateController类似的逻辑。 通过选择风格,输入描述词进行生图。 -class RolePhotoGenerateController: RoleCreateBaseController { - - var aiId: Int? - var introduction: String? - var sex: Sex? - var birthday: Int? - var birthdayString: String? - - // 可选 - var preSelectstyleCode: String? // 生图Style - var prePhotoDesc: String? // Description - - /// 剩余二创 - var countResponse: UserCreateCountResponse? - - var regenerateType: RolePhotoGenerateType = .album - - var confirmSelectAction: ((String, String, [AIUserImageQuery]?)-> Void)? - - override class var shouldPresentThisVc: Bool { - return true - } - - convenience init(type: RolePhotoGenerateType){ - self.init() - regenerateType = type - } - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[weak self] in - self?.container.scrollSelectedShow() - } - } - - private func setupViews() { - navigationView.alpha0Title = container.titleView.title - } - - private func setupDatas() { - loadCreateAlbumLeftCount() - - if let imageDesc = prePhotoDesc { - container.figureDescriptionContent = imageDesc - } - - if let styleCode = preSelectstyleCode{ - container.selectStyleId = styleCode - } - - - #if DEBUG - testCreationUIStates() - #endif - } - - private func loadCreateAlbumLeftCount(_ complete: ((Bool)-> Void)? = nil) { - Hud.showIndicator() - UserProvider.request(.getUserCreateCount, modelType: UserCreateCountResponse.self) { [weak self] result in - Hud.hideIndicator() - switch result { - case let .success(model): - self?.countResponse = model - self?.handleCountResponse(data: model) - complete?(true) - case .failure: - complete?(false) - } - } - } - - private func setupEvents() { - container.bottomButton.addTarget(self, action: #selector(bottomButtonTapped), for: .touchUpInside) - container.generate1Button.addTarget(self, action: #selector(tapAIGenerated), for: .touchUpInside) - - NotificationCenter.default.addObserver(self, selector: #selector(notifyCreateChanged), name: AppNotificationName.vipStateChange.notificationName, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(notifyCreateChanged), name: AppNotificationName.buyCreditsOnce.notificationName, object: nil) - - } - - // MARK: - Functions - - private func handleCountResponse(data: UserCreateCountResponse?) { - guard let countModel = data else { return } - let leftFreeNum = max((countModel.freeNum ?? 0) - (countModel.usedFreeNum ?? 0), 0) - let leftMemberNum = max((countModel.memberNum ?? 0) - (countModel.usedMemberNum ?? 0), 0) - let leftBuyNum = max((countModel.buyNum ?? 0) - (countModel.usedBuyNum ?? 0), 0) - - let isMemeber = UserCore.shared.user?.isMember.boolValue ?? false - - container.handleCreationUI(freeNum: leftFreeNum, memberNum: leftMemberNum, buyNum: leftBuyNum, isMember: isMemeber) -// #warning("test") -// container.handleCreationUI(freeNum: 0, memberNum: 0, buyNum: 0, isMember: false) - } - - private func goAlbumsResultVc(_ batchNo: String, _ imageStyleCode: String, _ description: String) { - let vc = RolePhotoResultsPickController()//RolePhotoResultPickController() - vc.batchNo = batchNo - vc.aiId = aiId - vc.regenerateType = regenerateType - vc.tapRegenerateAction = { [weak self] in - self?.bottomButtonTapped(regenerate: true) - } - - vc.confirmSelectAction = {[weak self] imgQuerys in - let styleCode = self?.container.selectStyleId ?? "" - let picDescription = self?.container.figureDescriptionContent ?? "" - - self?.confirmSelectAction?(styleCode, picDescription, imgQuerys) - } - - navigationController?.pushViewController(vc, animated: true) - } - - private func refreshBatchNoPhotos(_ batchNo: String, _ imageStyleCode: String, _ description: String) { - guard let navc = UIWindow.getTopNavigationController() else { - return - } - if let last = navc.viewControllers.last, last.isKind(of: RolePhotoResultsPickController.self), let resultVc = last as? RolePhotoResultsPickController { - resultVc.refreshToken(photosNo: batchNo) - } else { - dlog("❌Check下RolePhotoGenerateController") - } - } - - // MARK: - Action - - @objc private func tapAIGenerated() { - view.endEditing(true) - - if container.figureDescriptionContent.count > 0 { - let alert = Alert(title: "内容覆盖", text: "AI创建的内容会覆盖你已经填写的内容,请确认是否继续?") - let action1 = AlertAction(title: "继续", actionStyle: .confirm) { [weak self] in - self?.doGenerateAlbumDesc() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } else { - doGenerateAlbumDesc() - } - } - - private func doGenerateAlbumDesc() { - var params = [String: Any]() - - // 需要introduction - if let introduction = introduction { - params.updateValue(introduction, forKey: "introduction") - } - - if let sex = sex{ - params.updateValue(sex.rawValue, forKey: "sex") - } - - if let birthday = birthday{ - let date = Date.dateFromMilliseconds(Int64(birthday)) - let birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - params.updateValue(birthdayStr, forKey: "birthday") - } - - if let birthdayStr = birthdayString{ - params.updateValue(birthdayStr, forKey: "birthday") - } - - /// 形象描述 - // 新需求: 总是覆盖 - params.updateValue("GEN_AI_IMAGE_DESC_BY_NON", forKey: "ptType") - if container.figureDescriptionContent.count > 0 { - params.updateValue(container.figureDescriptionContent, forKey: "content") - } - - Hud.showIndicator() - AICowProvider.request(.aiContentGenerate(params: params), modelType: AIUserContentGenResponse.self) { [weak self] result in - Hud.hideIndicator() - switch result { - case let .success(success): - if let content = success?.content { - self?.container.figureDescriptionAIGenerated = content - } - case .failure: - return - } - } - } - - /// 创建生成图片 - @objc private func bottomButtonTapped(regenerate: Bool = false) { - guard let state = container.createState else { return } - - if state == .nonMemberNoQuota { - // 去买会员 - CLPurchase.shared.showVIPSubscribeSheet() - return - } else if state == .vipUserNoQuota { - // 买次数 - CLPurchase.shared.showBuyPhotoGeneratedCountSheet() - return - } - - var params = [String: Any]() - - if let aiUserId = aiId { - params.updateValue(aiUserId, forKey: "aiId") - } - - if let sex = sex { - params.updateValue(sex.rawValue, forKey: "sex") - } - - if let birthday = birthday { - let date = Date.dateFromMilliseconds(Int64(birthday)) - let birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - params.updateValue(birthdayStr, forKey: "birthday") - } - - if let birthdayStr = birthdayString{ - params.updateValue(birthdayStr, forKey: "birthday") - } - - // imageStylePrompt 形象风格提示词(预设的几种) - let imageStylePrompt = container.selectStylePrompt - let imageStyleCode = container.selectStyleId - params.updateValue(imageStylePrompt, forKey: "imageStylePrompt") - - // content:用户形象描述 or 生成的 - let description = container.descriptionTextView.textView.text.trimmed - params.updateValue(description, forKey: "content") - - params.updateValue(AIImageGenerateType.ALBUM.rawValue, forKey: "type") - - params.updateValue(true, forKey: "hl") - - Hud.showIndicator() - AICowProvider.request(.imageGenerateCreateTask(params: params), modelType: AIUserImageTaskCreateResponse.self) { [weak self] result in - Hud.hideIndicator() - switch result { - case let .success(success): - if let response = success, let batchNo = response.batchNo { -// self?.generatedBatchNoAction?(batchNo, imageStyleCode, description) -// self?.viewModel.batchNo = batchNo -// self?.navigationController?.popViewController(animated: true) - if regenerate { - self?.refreshBatchNoPhotos(batchNo, imageStyleCode, description) - } else { - self?.goAlbumsResultVc(batchNo, imageStyleCode, description) - } - - self?.loadCreateAlbumLeftCount() - } - case let .failure(failure): - dlog(failure) - } - } - } - - // MARK: - Noti - - @objc private func notifyCreateChanged(){ - Hud.showIndicator() - loadCreateAlbumLeftCount { result in - Hud.hideIndicator() - } - } - - // MARK: - 测试方法 - - var currentIndex = 0 - let testCases: [(freeNum: Int, memberNum: Int, buyNum: Int, isMember: Bool, description: String)] = [ - (5, 0, 0, false, "1免费用户有剩余次数"), - (0, 3, 0, true, "2会员用户有剩余次数"), - (0, 0, 2, false, "3购买用户有剩余次数"), - (0, 0, 0, false, "4非会员次数用尽"), - (0, 0, 0, true, "5会员用户次数用尽"), - ] - - /// 测试不同状态的UI显示 - /// 用于快速验证各种用户状态下的界面表现 - private func testCreationUIStates() { - navigationView.rightButton.isHidden = false - navigationView.rightButton.setTitle("Test", for: .normal) - navigationView.rightButton.setTitleColor(.white, for: .normal) - navigationView.paddingRightForRightStack = 24 - navigationView.rightButton.addTarget(self, action: #selector(switchTestState), for: .touchUpInside) - - // 显示第一个测试状态 - // showTestState(at: currentIndex) - } - - @objc private func switchTestState() { - showTestState(at: currentIndex) - - let nextIndex = (currentIndex + 1) % testCases.count - currentIndex = nextIndex - } - - private func showTestState(at index: Int) { - let testCase = testCases[index] - let state = container.handleCreationUI( - freeNum: testCase.freeNum, - memberNum: testCase.memberNum, - buyNum: testCase.buyNum, - isMember: testCase.isMember - ) - - // 更新导航栏标题显示当前测试状态 - navigationItem.title = "测试: \(testCase.description)" - - // 打印状态信息到控制台 - print(" 测试状态 \(index + 1)/\(testCases.count): \(testCase.description)") - print(" 参数: freeNum=\(testCase.freeNum), memberNum=\(testCase.memberNum), buyNum=\(testCase.buyNum), isMember=\(testCase.isMember)") - print(" 状态: \(state)") - - // 显示Toast提示 - view.hideToast() - view.makeToast("测试状态: \(testCase.description)", duration: 10.0, position: .top) - } - - /// 快速测试单个状态 - /// - Parameters: - /// - freeNum: 免费次数 - /// - memberNum: 会员次数 - /// - buyNum: 购买次数 - /// - isMember: 是否为会员 - /// - description: 状态描述 - private func quickTestState(freeNum: Int, memberNum: Int, buyNum: Int, isMember: Bool, description: String) { - let state = container.handleCreationUI( - freeNum: freeNum, - memberNum: memberNum, - buyNum: buyNum, - isMember: isMember - ) - - print("🧪 快速测试: \(description)") - print(" 状态: \(state)") - - view.makeToast("测试: \(description)", duration: 2.0, position: .top) - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoResultsPickController.swift b/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoResultsPickController.swift deleted file mode 100644 index ac54f6f..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoResultsPickController.swift +++ /dev/null @@ -1,263 +0,0 @@ -// -// RolePhotoResultsPickController.swift -// Crush -// -// Created by Leon on 2025/9/19. -// - -import UIKit -import Combine - -/// 二创选图 -class RolePhotoResultsPickController: RoleCreateBaseController { - var helpGhostButton: EPIconGhostButton! - - var batchNo: String! = "" - var aiId: Int! - - var isAllImagesOK: Bool = false - - var regenerateType: RolePhotoGenerateType = .album - - var tapRegenerateAction: (()-> Void)? - - var confirmSelectAction:(([AIUserImageQuery]?) -> Void)? - - private var cancellables = Set() - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - container.regenerateType = regenerateType - - setupViews() - setupDatas() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - disabledFullScreenPan() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - enabledFullScreenPan() - } - - private func setupViews() { - navigationView.alpha0Title = "Confirm the work" - navigationView.tapBackButtonAction = {[weak self] in - self?.alertToExitNotSaved(dismissFirst: true) - } - - if (regenerateType == .album){ - helpGhostButton = { - let v = EPIconGhostButton(radius: .none, iconSize: .medium, iconCode: .faq) - v.addTarget(self, action: #selector(tapHelpButton), for: .touchUpInside) - navigationView.rightStackH.addArrangedSubview(v) - navigationView.paddingRightForRightStack = 16 - v.snp.makeConstraints { make in - make.size.equalTo(v.bgImageSize()) - } - return v - }() - } - } - - private func setupDatas(){ - if batchNo.count > 0{ - container.setupLastGenerateProcessStart() - loadingGeneratingResults(onbatchNo: batchNo) - } - } - - private func setupEvents(){ - container.backButton.addTarget(self, action: #selector(tapBackButton), for: .touchUpInside) - container.headView.generateButton.addTarget(self, action: #selector(tapRegenerate), for: .touchUpInside) - container.bottomButton.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - } - - // MARK: - Public - - func refreshToken(photosNo: String?){ - self.batchNo = photosNo - dlog("重新生成中相册图片中...") - isAllImagesOK = false - container.setupLastGenerateProcessStart() - loadingGeneratingResults(onbatchNo: photosNo) - } - - // MARK: - Functions - func loadingGeneratingResults(onbatchNo : String?){ - if isAllImagesOK { - return - } - - guard let queryNo = onbatchNo, queryNo.isEmpty == false else{ - dlog("❌batchNo is nil") - return - } - - if let lastBatchNo = batchNo, lastBatchNo != queryNo{ - dlog("⚠️换新的batchNo了,退出之前的查询") - return - } - - dlog("📖查询图片生成中...") - AICowProvider.request(.imageGeneratedQuery(batchNo: queryNo), modelType: [AIUserImageQuery].self) { [weak self] result in - switch result { - case .success(let success): - guard let queryDatas = success else{ - return - } - self?.container.config(success) - var ok = true - if(queryDatas.count < 6){ - ok = false - }else{ - for per in queryDatas { - if per.status == .pending{ - ok = false - } - } - } - self?.isAllImagesOK = ok - if ok == false{ - // ⚠️ seconds delay to query batch images - DispatchQueue.main.asyncAfter(deadline: .now() + 6) {[weak self] in - self?.loadingGeneratingResults(onbatchNo: queryNo) - } - }else{ - // 都结束生成流程 - self?.batchNo = "" - self?.container.setupLastGenerateProcessEnd() - } - case .failure: - return - } - } - - } - - // MARK: - Functions - @objc private func doAddPicsToAlbum(){ - var params = [String:Any]() - - //... - var request = AlbumPhotoBatchAddRequest() - request.aiId = aiId - var images = [AlbumPhotoBatchAddImage]() - - for per in container.selectImgs { - var item = AlbumPhotoBatchAddImage() - item.url = per.imageUrl ?? "" - item.unlockPrice = per.unlockPrice ?? 0 - images.append(item) - } - -// for per in container.appearanceResult.selectDatas { -// var item = AlbumPhotoBatchAddImage() -// item.url = per.imageUrl ?? "" -// item.unlockPrice = container.price -// images.append(item) -// } - request.images = images - - - params = request.toNonNilDictionary() - - //dlog("params: \(params)") - - Hud.showIndicator() - AIRoleProvider.request(.aiRoleBatchAddAlbum(params: params), modelType: EmptyModel?.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - NotificationCenter.post(name: .aiRoleAlbumAddOrDelete) - self?.close(dismissFirst: true) - case .failure: - break - } - } - } - - private func doConfirmSelectThis(){ - let imgs = container.selectImgs - guard imgs.count > 0 else{ - return - } - - confirmSelectAction?(imgs) - self.close(dismissFirst: true) - } - - // MARK: - Action - /// 批量添加到相册 - @objc private func tapBottomButton(){ - switch regenerateType { - case .album: - doAddPicsToAlbum() - case .figure: - doConfirmSelectThis() - } - } - - - @objc private func tapHelpButton(){ - let title = "Instructions" - let content = "Interviewers can unlock the characters' pictures through paid methods, which can increase the creator's income share\nCrushlevel platform will share 20% of the sales revenue of each picture as the platform service fee\nSet up several free images that can attract interlocutors to interact with your virtual characters" - - let vc = UniversalInstructionsController() - vc.pageTitle = title - vc.content = content - navigationController?.pushViewController(vc, animated: true) - } - - @objc private func tapBackButton(){ - alertToExitNotSaved(dismissFirst: false) - } - - @objc private func alertToExitNotSaved(dismissFirst: Bool) { - let alert = Alert(title: "放弃创作", text: "选择退出或重新生图片,已经创作的图片将消失,同时消耗1次创作次数。") - let action1 = AlertAction(title: "确定", actionStyle: .destructive) { [weak self] in - self?.close(dismissFirst: dismissFirst) - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } - - @objc private func tapRegenerate() { - let alert = Alert(title: "放弃创作", text: "选择退出或重新生图片,已经创作的图片将消失,同时消耗1次创作次数。") - let action1 = AlertAction(title: "重新生成", actionStyle: .confirm) { [weak self] in - self?.doRegenerate() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel){ - // ... - } - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } - - @objc private func doRegenerate(){ - tapRegenerateAction?() - } - -} - -extension RolePhotoResultsPickController:RolePhotoGenerateSetGridCellDelegate{ - func generateSetCellTapPrice(obj: AIUserImageQuery?) { - let vc = RolePhotoUnlockWaySetController() - vc.albumGenerated = obj - vc.priceUpdateAction = { [weak self] price in - let unlockPrice = price ?? 0 - obj?.unlockPrice = unlockPrice - self?.container.reloadCv() - } - navigationController?.pushViewController(vc, animated: true) - } - -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoUnlockWaySetController.swift b/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoUnlockWaySetController.swift deleted file mode 100644 index a6e74ad..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/RolePhotoUnlockWaySetController.swift +++ /dev/null @@ -1,279 +0,0 @@ -// -// RolePhotoUnlockWaySetController.swift -// Crush -// -// Created by Leon on 2025/7/26. -// - -import UIKit -import Combine - -fileprivate let minCoin = 2000 -fileprivate let maxCoin = 9999900 - -/// 通用设置图片解锁价格 vc -class RolePhotoUnlockWaySetController: CLViewController { - - // Option 1: From 个人AI相册大图浏览 - var browseModel: PhotoBrowserModel? - // Option 2: From Album AI generated - var albumGenerated: AIUserImageQuery? - - var priceUpdateAction: ((_ price: Int?) -> Void)? - - @Published var selectUnLockway: Int = 0 - @Published var price = 0 - - // Config - - - - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDats() - setupEvents() - } - - private func setupViews() { - navigationView.alpha0Title = "Unlock Method" - container.container.scrollView.delegate = self - } - - private func setupDats() { - restoreData() - } - - private func restoreData(){ - if let photo = browseModel { - price = photo.aiAlbum.unlockPrice - if price > 0{ - selectUnLockway = 1 - } - } - else if let album = albumGenerated { - price = album.unlockPrice ?? 0 - if price > 0 { - selectUnLockway = 1 - } - }else{ - assert(false) - } - - } - - private func setupEvents() { - $price.sink {[weak self] price in - let coin = Coin(cents: price) - self?.container.priceTextField.textfield.text = coin.formatted -// self?.container.priceTextField.textfield.text = "\(String.thousandString(float: Double(price)/100.0))" - }.store(in: &cancellables) - - $selectUnLockway.sink {[weak self] unlockway in - guard let `self` = self else { - return - } - if unlockway == 0{ - self.price = 0 - self.container.selectView.contentStr = "Free" - self.container.priceTextField.isHidden = true - }else{// 1 - let coin = Coin(input: self.container.priceTextField.textfield.text ?? "") - if coin.cents > 0{ - self.price = coin.cents - }else{ - self.price = minCoin - } - self.container.selectView.contentStr = "Paid" - self.container.priceTextField.isHidden = false - } - }.store(in: &cancellables) - - container.selectView.selectBlock = {[weak self] in - self?.presentPickUnlockWay() - } - - container.priceTextField.textfield.textDidChanged = {[weak self] textfield in - guard let self = self else{return} - - let coin = Coin(input: textfield.text) - let cents = coin.cents - if cents > maxCoin{ - textfield.text = Coin(cents: maxCoin).formatted - self.price = maxCoin - } - } - - container.priceTextField.textfield.textDidEndEditing = {[weak self] textfield in - guard let self = self else{return} - - let priceUsd = Int(textfield.text ?? "1") ?? 1 - self.price = priceUsd * 100 - if self.price < minCoin{ - textfield.text = Coin(cents: minCoin).formatted - self.price = minCoin - } - } - - container.bottomButton.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - } - - // MARK: Helper - - private func presentPickUnlockWay(){ - let vc = SelectCommonController() - var models = [SelectCommonModel]() - - var selectModel : SelectCommonModel! - do{ - let model = SelectCommonModel() - model.name = "Free" - model.objInfo = 0 - models.append(model) - if(selectUnLockway == 0){ - selectModel = model - } - } - do{ - let model = SelectCommonModel() - model.name = "Paid" - model.objInfo = 1 - models.append(model) - if(selectUnLockway == 1){ - selectModel = model - } - } - - vc.datas = models - - vc.markSelected = [selectModel] - - presentNaviRootVc(vc: vc) - vc.tapSingleAction = {[weak self] obj in - dlog("select obj : \(obj)") - if let model = obj as? SelectCommonModel { - self?.selectUnLockway = model.objInfo as! Int - } - } - } - - // MARK: - Action - - @objc private func tapBottomButton() { - if let photo = browseModel{ - let albumId = photo.aiAlbum.albumId - - Hud.showIndicator() - AIRoleProvider.request(.aiRolePhotoUnlockPriceSet(aiUid: photo.aiId, albumId: albumId, unlockPrice: price), modelType: Bool?.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.priceUpdateAction?(self?.price) - NotificationCenter.post(name: .aiRoleAlbumPhotoInfoChanged) - self?.close() - case .failure: - break - } - } - }else if albumGenerated != nil{ - priceUpdateAction?(self.price) - close() - } - } -} - -extension RolePhotoUnlockWaySetController: UIScrollViewDelegate{ - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} - -class RolePhotoUnlockWaySetView: UIView { - var bottomButton: StyleButton! - var container: LTScrollContainer! - var titleView: TitleView! - var selectView: CLSelectView! - var priceTextField: TitleTextField! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 0 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - selectView = { - let v = CLSelectView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - priceTextField = { - let v = TitleTextField() - v.textfield.setupLeftCoinIconView() - v.textfield.keyboardType = .numberPad - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - titleView.title = "Unlock Method" - selectView.titleLabel.text = "Unlock Method" - selectView.placeholder = "Unlock Method" - priceTextField.titleLabel.text = "Unlock Price" - let minCoinShow = Coin(cents: minCoin).formatted - let maxCoinShow = Coin(cents: maxCoin).formatted - priceTextField.placeholder = "\(minCoinShow)-\(maxCoinShow)" - bottomButton.setTitle("Save", for: .normal) - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoGenerateView.swift b/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoGenerateView.swift deleted file mode 100644 index 4edec39..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoGenerateView.swift +++ /dev/null @@ -1,452 +0,0 @@ -// -// RolePhotoGenerateView.swift -// Crush -// -// Created by Leon on 2025/7/27. -// - -import UIKit -import Combine - -class RolePhotoGenerateView: UIView { - var bottomButton: StyleButton! - var tipStackH: UIStackView! - var tipIcon: UIImageView! - var tipLabel: ColorLabel! - var tipNormalLabel: CLLabel! - - var container: LTScrollContainer! - - var titleView: TitleView! - - // Stackview's subviews - var styleContainer:UIView! - var styleTipLabel:UILabel! - var styleCv:UICollectionView! - - var descriptionTextView: TitleTextView! - var generate1Button: TextButton! - - var referenceLabelsView: TitleAndSubTitleView! - - var datas:[ImageStylePic] = [] - @Published var selectStyleId: String = "" - var selectStylePrompt: String = "" - - @Published var figureDescriptionAIGenerated: String = "" - @Published var figureDescriptionContent: String = "" - - /// 编辑部分的内容可以去提交 - @Published var contentIsOKToSubmit:Bool = false - @Published var createState: AlbumCreationState? - - - private var cancellables = Set() - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupData(){ - setupStylesDatas() - } - - private func setupStylesDatas(){ - if let imageStyles = AppDictManager.shared.aiDict?.imageStyleDictList{ - datas = imageStyles - // 想要默认选中,请解锁 - if let first = datas.first{ - selectStyleId = first.code ?? "" - selectStylePrompt = first.prompt ?? "" - } - }else{ - Hud.showIndicator() - AppDictManager.shared.loadAIDict {[weak self] ok in - Hud.hideIndicator() - if(ok){ - self?.setupStylesDatas() - } - } - } - } - - private func setupEvent(){ - $contentIsOKToSubmit.sink {[weak self] enable in - self?.bottomButton.isEnabled = enable - }.store(in: &cancellables) - - $selectStyleId.sink {[weak self] code in - guard let `self` = self else { - return - } - - guard let imageStyles = AppDictManager.shared.aiDict?.imageStyleDictList else{return} - self.selectStylePrompt = imageStyles.first(where: {$0.code == code})?.prompt ?? "" - - }.store(in: &cancellables) - - $figureDescriptionContent.sink {[weak self] str in - self?.descriptionTextView.defaultText = str - }.store(in: &cancellables) - - $figureDescriptionAIGenerated.sink {[weak self] str in - self?.descriptionTextView.textView.text = str - self?.figureDescriptionContent = str - }.store(in: &cancellables) - - descriptionTextView.textDidChanged = {[weak self] textField in - self?.figureDescriptionContent = textField.text - } - - Publishers.CombineLatest3($figureDescriptionContent, $selectStyleId, $createState).map{ - content, selectStyleCode, state in - if content.trimmed.count > 0 && selectStyleCode.count > 0{ - return true - } - - if state == .nonMemberNoQuota || state == .vipUserNoQuota { - return true - } - - return false - }.assign(to: &$contentIsOKToSubmit) - -//#if DEBUG -//figureDescriptionContent = "一个高度拟真的人形 AI,造型流畅而未来感十足,金属质感的皮肤上有柔和发光的电路纹路,拥有富有表现力的合成眼睛,眼眸中散发柔和光芒,身穿极简高科技服饰并带有全息细节,周围环绕着漂浮的数据流和抽象几何图案,电影级灯光,超精细,8K 分辨率" -//#endif - } - - // MARK: - Public - - func scrollSelectedShow(){ - // 将选中的style的cell滑动显示出来 - if let index = self.datas.firstIndex(where: {$0.code == selectStyleId}){ - // print("currentindex : \(index)") - self.styleCv.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: false) - } - } - - // MARK: - Helper - - private func setupButtonVIPBuyStyle(){ - bottomButton.context(size: .large) - let buttonString = "Buy VIP +10/Month" - let aStr = getAttributeTitleByWords(words: buttonString) - bottomButton.setAttributedTitle(aStr, for: .normal) - } - - private func setupButtonNormalStyle(string: String){ - bottomButton.primary(size: .large) - bottomButton.setAttributedTitle(nil, for: .normal) - bottomButton.setTitle(string, for: .normal) - } - - private func getAttributeTitleByWords(words: String) -> NSAttributedString { - - let text = " \(words)" - let attributedString = NSMutableAttributedString() - if let iconImage = MWIconFont.image(fromIcon: .iconVip, size: CGSize(width: 24, height: 24), color: .black) { // 替换为你的图标名称 - let attachment = NSTextAttachment() - attachment.image = iconImage - - let iconSize = CGSize(width: 24, height: 24) // 调整为你需要的大小 - attachment.bounds = CGRect(origin: .init(x: 0, y: -4), size: iconSize) - - let iconAttributedString = NSAttributedString(attachment: attachment) - attributedString.append(iconAttributedString) - } - - // 添加文字并设置样式 - let textAttributedString = text.withAttributes([ - .font(.t.tbsl), // 设置字体 - .textColor(.black), - ]) - attributedString.append(textAttributedString) - return attributedString - } - - // MARK: - UI - - private func setupViews() { - bottomButton = { - let v = StyleButton(type: .custom) - //v.context(size: .large) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - - tipStackH = { - let v = UIStackView() - v.spacing = 8 - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.leading.greaterThanOrEqualToSuperview().offset(24) - make.trailing.lessThanOrEqualToSuperview().offset(-24) - make.centerX.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - tipIcon = { - let v = UIImageView() - v.image = UIImage(named: "vip_flag_16") - tipStackH.addArrangedSubview(v) - v.isHidden = true - return v - }() - - tipLabel = { - let v = ColorLabel() - v.applyGradient(.vip) - v.font = .t.tls - // v.textAlignment = .center - tipStackH.addArrangedSubview(v) - v.isHidden = true - return v - }() - - tipNormalLabel = { - let v = CLLabel() - v.font = .t.tls - tipStackH.addArrangedSubview(v) - v.isHidden = true - return v - }() - - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(tipLabel.snp.top).offset(-16) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 16 - v.optionInnerTopPadding = 16 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - container.stack.setCustomSpacing(0, after: titleView) - - styleContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - styleTipLabel = { - let v = UILabel() - styleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview().offset(12) - } - v.font = .t.tlm - v.textColor = .c.ctpn - v.text = "Style" - return v - }() - - styleCv = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - let size = CGSize(width: 100, height: 100 + 24) - layout.itemSize = size - layout.minimumLineSpacing = 12 - layout.minimumInteritemSpacing = 12 - layout.sectionInset = UIEdgeInsets(top: 0, left: CGFloat.lrs, bottom: 0, right: CGFloat.lrs) - let v = UICollectionView(frame: .zero, collectionViewLayout: layout) - v.showsHorizontalScrollIndicator = false - styleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(styleTipLabel.snp.bottom).offset(12) - make.height.equalTo(size.height) - make.bottom.equalToSuperview() - } - v.backgroundColor = .clear - v.register(RoleFigureStyleCell.self, forCellWithReuseIdentifier: "RoleFigureStyleCell") - v.dataSource = self - v.delegate = self - return v - }() - - descriptionTextView = { - let v = TitleTextView() - // 10~500 - v.maxLimit = 1000 -// v.minLimit = 10 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - - return v - }() - - generate1Button = { - let v = TextButton() - v.text = "自动生成" - descriptionTextView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalTo(descriptionTextView.titleLabel) - make.height.equalTo(40) - } - return v - }() - - referenceLabelsView = { - let v = TitleAndSubTitleView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - titleView.title = "Create" - tipLabel.text = "VIP Remaining Times 0/10" - descriptionTextView.titleLabel.text = "Description" - descriptionTextView.placeholder = "请描述形象的肤色、服饰、发型、五官、动作、背景等" - //descriptionTextView.errorMsg = "只能包含10-500字符" - bottomButton.setTitle("Submit", for: .normal) - referenceLabelsView.titleLabel.text = "Reference" - referenceLabelsView.subLabel.text = "AI will create based on the basic image of the character" - } -} - -enum AlbumCreationState: Equatable { - case freeUserWithQuota(Int) // 免费用户且有剩余次数 (freeNum) - case vipUserWithQuota(Int) // 会员用户且有剩余次数 (memberNum) - case buyerWithQuota(Int) // 购买用户且有剩余次数 (buyNum) - case nonMemberNoQuota // 非会员,次数用尽 - case vipUserNoQuota // 会员用户,次数用尽 -} - -// MARK: 处理 -extension RolePhotoGenerateView{ - - - func getCreationState(freeNum: Int, memberNum: Int, buyNum: Int, isMember: Bool) -> AlbumCreationState { - if freeNum > 0 { - return .freeUserWithQuota(freeNum) - } else if memberNum > 0 { - return .vipUserWithQuota(memberNum) - } else if buyNum > 0 { - return .buyerWithQuota(buyNum) - } else if !isMember { - return .nonMemberNoQuota - } else { - return .vipUserNoQuota - } - } - - @discardableResult - func handleCreationUI(freeNum: Int, memberNum: Int, buyNum: Int, isMember: Bool) -> AlbumCreationState { - let state = getCreationState(freeNum: freeNum, memberNum: memberNum, buyNum: buyNum, isMember: isMember) - - tipIcon.isHidden = true - tipLabel.isHidden = true - tipNormalLabel.isHidden = true - - let totalLeftUseNum = freeNum + memberNum + buyNum - - switch state { - case .freeUserWithQuota(let free): - print("🎨免费用户,还剩 \(free) 次") // 彩色vip + create - // Top: - tipIcon.isHidden = false - tipLabel.isHidden = false - tipLabel.text = "Non-members have 10 free creations (\(totalLeftUseNum))" - setupButtonNormalStyle(string: "Create") - - case .vipUserWithQuota(let vip): - print("🎨会员用户,还剩 \(vip)/10 次") // 白色。+ Create - tipNormalLabel.isHidden = false - tipNormalLabel.text = "VIP Remaining Times (\(totalLeftUseNum))" - tipNormalLabel.textColor = .c.ctpn - setupButtonNormalStyle(string: "Create") - - case .buyerWithQuota(let buy): // 白色+ Create - print("🎨购买用户,还剩 \(buy) 次") - tipNormalLabel.isHidden = false - tipNormalLabel.textColor = .c.ctpn - tipNormalLabel.text = "VIP Remaining Times (\(totalLeftUseNum))" - setupButtonNormalStyle(string: "Create") - case .nonMemberNoQuota: - print("🎨非会员,次数已用完,提示开通会员") - tipIcon.isHidden = false - tipLabel.isHidden = false - tipLabel.text = "Non-members have 10 free creations (\(0))" - // -> 开通会员 - setupButtonVIPBuyStyle() - case .vipUserNoQuota: - print("🎨会员用户,次数已用完,提示购买额外次数") - tipNormalLabel.isHidden = false - tipNormalLabel.textColor = .c.cwvn - tipNormalLabel.text = "VIP Remaining Times (0)" - - // -> 购买次数 - setupButtonNormalStyle(string: "Buy Times") - } - - createState = state - return state - } - - -} - -extension RolePhotoGenerateView:UICollectionViewDataSource, UICollectionViewDelegate{ - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoleFigureStyleCell", for: indexPath) as! RoleFigureStyleCell - let item = datas[indexPath.item] - cell.setupData(data: item) - cell.setupSelected(selected: item.code == selectStyleId) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let item = datas[indexPath.item] - selectStyleId = item.code ?? "" - // selectStylePrompt = item.prompt ?? "" - collectionView.reloadData() - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoResultSettingViews.swift b/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoResultSettingViews.swift deleted file mode 100644 index 91471b9..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoResultSettingViews.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// RolePhotoResultSettingViews.swift -// Crush -// -// Created by Leon on 2025/9/19. -// - -protocol RolePhotoGenerateSetGridCellDelegate: AnyObject { - func generateSetCellTapPrice(obj: AIUserImageQuery?) -} - -class RolePhotoGenerateSetGridCell: RoleCreateAppearanceResultCell { - weak var delegate: RolePhotoGenerateSetGridCellDelegate? - - var regenerateType: RolePhotoGenerateType = .album{ - didSet{ - if regenerateType == .figure{ - lockWayView.isHidden = true - } - } - } - - var lockWayView: UnlockWaySetFlagView! - override init(frame: CGRect) { - super.init(frame: frame) - setupGridViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupGridViews() { - lockWayView = { - let v = UnlockWaySetFlagView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(8) - make.bottom.equalToSuperview().offset(-8) - } - return v - }() - - lockWayView.topButton.addTarget(self, action: #selector(tapUnlockButton), for: .touchUpInside) - } - - override func config(data: AIUserImageQuery) { - super.config(data: data) - - lockWayView.config(data.unlockPrice ?? 0) - } - - override func setupSelected(_ selected: Bool) { - super.setupSelected(selected) - - selectMark.image = UIImage(named: "checkmark_tick") - -// if regenerateType == .album{ -// selectMark.isHidden = false -// selectMark.image = selected ? UIImage(named: "checkmark_tick") : UIImage(named: "checkmark_tick_light") -// }else { -// selectMark.image = UIImage(named: "checkmark_tick") -// } - } - - @objc private func tapUnlockButton() { - delegate?.generateSetCellTapPrice(obj: data) - } -} - -class UnlockWaySetFlagView: UIView { - var stackH: UIStackView! - - var iconAndLabel: CLIconLabel! - var settingIcon: UIImageView! - var topButton: UIButton! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.csedn - - snp.makeConstraints { make in - make.height.equalTo(32) - } - stackH = { - let v = UIStackView() - v.spacing = 8 - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-8) - make.leading.equalToSuperview().offset(12) - } - return v - }() - - iconAndLabel = { - let v = CLIconLabel() - v.spacing = 4 - v.iconImageView.image = UIImage.icon16Diamond - v.contentLabel.font = .t.tls - stackH.addArrangedSubview(v) - return v - }() - - settingIcon = { - let v = UIImageView() - v.image = MWIconFont.image(fromIcon: .iconSetting, size: CGSize(width: 12, height: 12), color: .white) - stackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 12, height: 12)) - } - return v - }() - - topButton = { - let v = UIButton() - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - iconAndLabel.iconImageView.isHidden = true - iconAndLabel.contentLabel.text = "Free" - } - - func config(_ price: Int) { - if price > 0 { - iconAndLabel.iconImageView.isHidden = false - let coin = Coin(cents: price) - iconAndLabel.contentLabel.text = coin.thousandthsFormatted - } else { - iconAndLabel.iconImageView.isHidden = true - iconAndLabel.contentLabel.text = "Free" - } - } - - override func layoutSubviews() { - super.layoutSubviews() - cornerRadius = bounds.size.height * 0.5 - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoResultsPickView.swift b/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoResultsPickView.swift deleted file mode 100644 index a073105..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleAlbumPhoto/View/RolePhotoResultsPickView.swift +++ /dev/null @@ -1,290 +0,0 @@ -// -// RolePhotoResultMultiPickView.swift -// Crush -// -// Created by Leon on 2025/9/19. -// -import Combine - -class RolePhotoResultsPickView: CLContainer { - var regenerateType: RolePhotoGenerateType = .album - - var backButton: EPIconTertiaryButton! - var bottomButton: StyleButton! - - var layout = UICollectionViewFlowLayout() - var cv: UICollectionView! - - var headView = RolePhotoResultMultiPickHeadView() - var headHeight = 104.0 + 12.0 - - var datas = [AIUserImageQuery]() - - // var selectImgUrls = [String]() - @Published var selectImgs = [AIUserImageQuery]() - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .large, iconCode: .arrowLeftBorder) - let size = v.bgImageSize() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - make.size.equalTo(CGSize(width: 88, height: size.height)) - } - return v - }() - - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - // make.leading.equalToSuperview().offset(CGFloat.lrs) - make.leading.equalTo(backButton.snp.trailing).offset(16) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - v.isEnabled = false - return v - }() - - cv = { - layout = UICollectionViewFlowLayout() - let itemW = floor((UIScreen.width - 24 * 2 - 16) * 0.5) - let itemH = itemW * 219 / 163.0 - layout.itemSize = CGSize(width: itemW, height: itemH) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 0, left: 24, bottom: 16, right: 24) - - let v = UICollectionView(frame: .zero, collectionViewLayout: layout) - v.backgroundColor = .clear - v.showsHorizontalScrollIndicator = false - v.showsVerticalScrollIndicator = false - v.register(RolePhotoGenerateSetGridCell.self, forCellWithReuseIdentifier: "RolePhotoGridCell") - v.register(UICollectionReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: "UICollectionReusableView") - v.dataSource = self - v.delegate = self - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return v - }() - - bottomButton.setTitle("Complete", for: .normal) - headView.titleView.title = "Confirm the work" - } - - private func setupEvent(){ - $selectImgs.sink {[weak self] imgs in - self?.bottomButton.isEnabled = imgs.count > 0 - }.store(in: &cancellables) - } - - // MARK: - Public - - func config(_ datas: [AIUserImageQuery]?) { - let validDatas = datas ?? [] - - if validDatas.count >= 1{ - self.datas = validDatas - }else{ - self.datas.removeAll(where: {$0.status != .completed}) - // 如果datas的数量不够6个,补齐datas的数量为6个,补充AIUserImageQuery() - if self.datas.count < 6{ - for _ in 0...5 - self.datas.count { - self.datas.append(AIUserImageQuery()) - } - } - } - - // Reselect - var tempSelectImages = [AIUserImageQuery]() - for perNew in validDatas { - for selectImg in selectImgs { - if selectImg.imageUrl == perNew.imageUrl{ - tempSelectImages.append(perNew) - } - } - } - selectImgs = tempSelectImages - - cv.reloadData() - } - - func reloadCv(){ - cv.reloadData() - } - - - public func setupLastGenerateProcessStart() { - headView.generateButton.isEnabled = false - } - - public func setupLastGenerateProcessEnd() { - headView.generateButton.isEnabled = true - } -} - -extension RolePhotoResultsPickView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RolePhotoGridCell", for: indexPath) as! RolePhotoGenerateSetGridCell - cell.regenerateType = regenerateType - cell.delegate = viewController() as? any RolePhotoGenerateSetGridCellDelegate - let data = datas[indexPath.item] - cell.config(data: data) - - let isSelected = selectImgs.contains(where: { $0 === data }) - cell.setupSelected(isSelected) - - cell.tapFullBrowseAction = { data, image in - guard let url = data.imageUrl, !url.isEmpty else { return } - - var photoModels = [PhotoBrowserModel]() - let model = PhotoBrowserModel() - model.image = image - model.sourceRect = cell.screenRect ?? .zero - model.imageUrl = url - photoModels.append(model) - #warning("to do, change .normal") - ImageBrowser.show(models: photoModels, index: 0, type: .normal) - } - - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let data = datas[indexPath.item] - - guard data.status == .completed else{ - return - } - - switch regenerateType{ - case .album: - if selectImgs.contains(where: { $0 === data }) { - selectImgs.removeAll { $0 === data } - } else { - // ⚠️ 都改成单选 - selectImgs.removeAll() - selectImgs.append(data) - } - case.figure: - if selectImgs.contains(where: { $0 === data }) { - selectImgs.removeAll { $0 === data } - } else { - selectImgs.removeAll() - selectImgs.append(data) - } - } - - - collectionView.reloadData() - selectImgs = selectImgs - } - - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { - let header = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: "UICollectionReusableView", - for: indexPath - ) - if headView.superview == nil || headView.superview != header { - headView.removeFromSuperview() - } - - header.addSubview(headView) - headView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - } - - return header - } - return UICollectionReusableView() - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - return CGSize(width: UIScreen.width, height: headHeight) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, alphaViews: [navigationView?.titleLabel]) - } -} - -class RolePhotoResultMultiPickHeadView: UIView { - var titleView: TitleView! - - var titleLabel: CLLabel! - var generateButton: TextButton! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - titleView = { - let v = TitleView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = CLLabel() - v.font = .t.tlm - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(titleView.snp.bottom).offset(12) - make.leading.equalToSuperview().offset(24) - } - return v - }() - - generateButton = { - let v = TextButton() - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.centerY.equalTo(titleLabel) - make.height.equalTo(40) - make.bottom.equalToSuperview() - } - return v - }() - titleLabel.text = "Appearance" - generateButton.text = "Regenerate" - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/ChatBackgroundGenerateController.swift b/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/ChatBackgroundGenerateController.swift deleted file mode 100644 index 2b75614..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/ChatBackgroundGenerateController.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// ChatBackgroundGenerateInputController.swift -// Crush -// -// Created by Leon on 2025/8/26. -// - -import UIKit - - -/// 聊天背景二创 -class ChatBackgroundGenerateController: CLViewController { - var aiId : Int? - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDats() - setupEvents() - } - - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[weak self] in - self?.container.scrollSelectedShow() - } - } - - private func setupViews() { - navigationView.alpha0Title = "Create" - - } - - private func setupDats(){ - - } - - private func setupEvents(){ - container.bottomButton.addTarget(self, action: #selector(bottomButtonTapped), for: .touchUpInside) - container.generate1Button.addTarget(self, action: #selector(tapAIGenerated), for: .touchUpInside) - } - - @objc private func bottomButtonTapped(regenerate:Bool = false) { - var params = [String:Any]() - - if let aiUserId = aiId{ - params.updateValue(aiUserId, forKey: "aiId") - } - - // imageStylePrompt 形象风格提示词(预设的几种) - let imageStylePrompt = container.selectStylePrompt - let imageStyleCode = container.selectStyleId - params.updateValue(imageStylePrompt, forKey: "imageStylePrompt") - - // content:用户形象描述 or 生成的 - let description = container.descriptionTextView.textView.text.trimmed - params.updateValue(description, forKey: "content") - - params.updateValue(AIImageGenerateType.BACKGROUND.rawValue, forKey: "type") - - params.updateValue(true, forKey: "hl") - - Hud.showIndicator() - AICowProvider.request(.imageGenerateCreateTask(params: params), modelType: AIUserImageTaskCreateResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - WalletCore.shared.refreshWallet() - - if let response = success, let batchNo = response.batchNo{ - if regenerate { - self?.refreshBatchNoPhotos(batchNo, imageStyleCode, description) - }else{ - self?.goResultVc(batchNo, imageStyleCode, description) - } - } - case .failure(let failure): - dlog(failure) - } - } - - - } - - // MARK: - Functions - private func goResultVc(_ batchNo: String, _ imageStyleCode: String, _ description: String){ - let vc = ChatBackgroundResultGridController() - vc.batchNo = batchNo - vc.aiId = aiId - vc.tapRegenerateAction = {[weak self] in - let alert = Alert(title: "Give up creation", text: "已经创作的图片将消失,不会退还已消耗的Crush Coin。") - let action1 = AlertAction(title: "重新生成", actionStyle: .confirm) { - self?.bottomButtonTapped(regenerate: true) - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } - navigationController?.pushViewController(vc, animated: true) - } - - private func refreshBatchNoPhotos(_ batchNo: String, _ imageStyleCode: String, _ description: String){ - guard let navc = UIWindow.getTopNavigationController() else{ - return - } - if let last = navc.viewControllers.last, last.isKind(of: ChatBackgroundResultGridController.self), let resultVc = last as? ChatBackgroundResultGridController{ - resultVc.refreshToken(photosNo: batchNo) - }else{ - dlog("❌Check下RolePhotoGenerateController") - } - } - - - @objc private func tapAIGenerated(){ - if container.figureDescriptionContent.count > 0{ - let alert = Alert(title: "内容覆盖", text: "AI创建的内容会覆盖你已经填写的内容,请确认是否继续?") - let action1 = AlertAction(title: "继续", actionStyle: .confirm) {[weak self] in - self?.doGenerateAlbumDesc() - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - }else{ - doGenerateAlbumDesc() - } - } - - private func doGenerateAlbumDesc(){ - - guard let aiInfo = IMAIViewModel.shared.aiIMInfo else{ - return - } - - var params = [String: Any]() - - if let aiId = aiInfo.aiId { - params.updateValue(aiId, forKey: "aiId") - } - - if let nickname = aiInfo.nickname{ - params.updateValue(nickname, forKey: "nickname") - } - - if let sex = aiInfo.sex{ - params.updateValue(sex.rawValue, forKey: "sex") - } - - if let birthday = aiInfo.birthday{ - let date = Date.dateFromMilliseconds(Int64(birthday)) - let birthdayStr = date.toString(dateFormat: "yyyy-MM-dd") - params.updateValue(birthdayStr, forKey: "birthday") - } - - if let introduction = aiInfo.introduction{ - params.updateValue(introduction, forKey: "introduction") - } - - let imageStylePrompt = container.selectStylePrompt -// let imageStyleCode = container.selectStyleId - params.updateValue(imageStylePrompt, forKey: "imageStylePrompt") - - /// 形象描述 - params.updateValue("GEN_AI_IMAGE_DESC_BY_NON", forKey: "ptType") - if container.figureDescriptionContent.count > 0 { - params.updateValue(container.figureDescriptionContent, forKey: "content") - } - - Hud.showIndicator() - AICowProvider.request(.aiContentGenerate(params: params), modelType: AIUserContentGenResponse.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let content = success?.content{ - self?.container.figureDescriptionAIGenerated = content - } - case .failure: - return - } - } - } - -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/ChatBackgroundResultGridController.swift b/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/ChatBackgroundResultGridController.swift deleted file mode 100644 index 94b40bd..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/ChatBackgroundResultGridController.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// ChatBackgroundResultGridController.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit - -class ChatBackgroundResultGridController: CLViewController { - var batchNo: String! - var aiId: Int! - - var tapRegenerateAction: (() -> Void)? - - // -- Flag - var isAllImagesOK: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - setupViews() - setupDatas() - setupEvents() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - disabledFullScreenPan() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - enabledFullScreenPan() - } - - private func setupViews() { - navigationView.alpha0Title = "Confirm the work" - navigationView.setupBackButtonCloseIcon() - navigationView.tapBackButtonAction = { [weak self] in - self?.alertToExitNotSaved(dismissFirst: true) - } - } - - private func setupDatas() { - Hud.showIndicator() - container.setupLastGenerateProcessStart() - loadingGeneratingResults(onbatchNo: batchNo) - } - - private func setupEvents() { - container.backButton.addTarget(self, action: #selector(tapBackButton), for: .touchUpInside) - container.headView.generateButton.addTarget(self, action: #selector(tapRegenerate), for: .touchUpInside) - container.bottomButton.addTarget(self, action: #selector(tapOperateButton), for: .touchUpInside) - } - - // MARK: - Public - - func refreshToken(photosNo: String?) { - self.batchNo = photosNo - dlog("重新生成聊天背景中...") - isAllImagesOK = false - container.setupLastGenerateProcessStart() - loadingGeneratingResults(onbatchNo: photosNo) - } - - // MARK: - Functions - - func loadingGeneratingResults(onbatchNo: String?) { - if isAllImagesOK { - return - } - - guard let queryNo = onbatchNo, queryNo.isEmpty == false else { - dlog("❌batchNo is nil") - return - } - - if let lastBatchNo = batchNo, lastBatchNo != queryNo { - dlog("⚠️换新的batchNo了,退出之前的查询") - return - } - - dlog("📖查询图片生成中...") - AICowProvider.request(.imageGeneratedQuery(batchNo: queryNo), modelType: [AIUserImageQuery].self) { [weak self] result in - Hud.hideIndicator() - switch result { - case let .success(success): - guard let queryDatas = success else { - return - } - self?.container.config(success) - var ok = true - if queryDatas.count < 6 { - ok = false - } else { - for per in queryDatas { - if per.status == .pending{ - ok = false - } - } - } - self?.isAllImagesOK = ok - if ok == false { - // ⚠️ seconds delay to query batch images - DispatchQueue.main.asyncAfter(deadline: .now() + 6) { [weak self] in - self?.loadingGeneratingResults(onbatchNo: queryNo) - } - }else{ - self?.batchNo = "" - self?.container.setupLastGenerateProcessEnd() - } - case .failure: - return - } - } - } - - // MARK: - Action - - @objc private func alertToExitNotSaved(dismissFirst: Bool) { - let alert = Alert(title: "Give up creation", text: "If you choose to opt out or regenerate the picture, the picture you have created will disappear and the Crush coin you have consumed will not be refunded.") - let action1 = AlertAction(title: "确定", actionStyle: .destructive) { [weak self] in - self?.close(dismissFirst: dismissFirst) - } - let action2 = AlertAction(title: "Cancel", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } - - @objc private func tapBackButton() { - //close() - alertToExitNotSaved(dismissFirst: false) - } - - @objc private func tapRegenerate() { - tapRegenerateAction?() - } - - @objc private func tapOperateButton() { - guard container.selectImgs.count > 0 else { - return - } - - var params = [String: Any]() - params.updateValue(aiId!, forKey: "aiId") - - var imgs = [[String:Any]]() - for per in container.selectImgs { - var image = TempBatchAddBackground() - image.url = per.imageUrl - imgs.append(image.toNonNilDictionary()) - } - - params.updateValue(imgs, forKey: "images") - - /* - 返回: - { - "ids" : [ - 13 - ] - } - */ - AIRoleProvider.request(.batchAddChatBackground(params: params), modelType: EmptyModel.self) { [weak self] result in - switch result { - case .success: - NotificationCenter.post(name: .chatSettingBackgroundListUpdated) - self?.close(dismissFirst: true) - case .failure: - break - } - } - } - - struct TempBatchAddBackground: Codable { - var url: String? - var width: String = "\(AppConst.gAIPhotoWidth)" - var height: String = "\(AppConst.gAIPhotoHeight)" - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundGenerateInputView.swift b/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundGenerateInputView.swift deleted file mode 100644 index 58cfb1c..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundGenerateInputView.swift +++ /dev/null @@ -1,325 +0,0 @@ -// -// ChatBackgroundGenerateInputView.swift -// Crush -// -// Created by Leon on 2025/8/26. -// -import UIKit -import Combine -class ChatBackgroundGenerateInputView: CLContainer{ - var bottomView : UIView! - var coinLabel : CLIconLabel! - var bottomButton: StyleButton! - - var container: LTScrollContainer! - - var titleView: TitleView! - - // Stackview's subviews - var styleContainer:UIView! - var styleTipLabel:UILabel! - var styleCv:UICollectionView! - - var descriptionTextView: TitleTextView! - var generate1Button: TextButton! - - var referenceLabelsView: TitleAndSubTitleView! - - var datas:[ImageStylePic] = [] - @Published var selectStyleId: String = "" - var selectStylePrompt: String = "" - - @Published var figureDescriptionAIGenerated: String = "" - @Published var figureDescriptionContent: String = "" - - @Published var buttonEnable:Bool = false - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupData(){ - setupStylesDatas() - } - - private func setupStylesDatas(){ - if let imageStyles = AppDictManager.shared.aiDict?.imageStyleDictList{ - datas = imageStyles - if let first = datas.first{ - selectStyleId = first.code ?? "" - selectStylePrompt = first.prompt ?? "" - } - }else{ - Hud.showIndicator() - AppDictManager.shared.loadAIDict {[weak self] ok in - Hud.hideIndicator() - if(ok){ - self?.setupStylesDatas() - } - } - } - } - - private func setupEvent(){ - $buttonEnable.sink {[weak self] enable in - self?.bottomButton.isEnabled = enable - }.store(in: &cancellables) - - $selectStyleId.sink {[weak self] code in - guard let `self` = self else { - return - } - - guard let imageStyles = AppDictManager.shared.aiDict?.imageStyleDictList else{return} - self.selectStylePrompt = imageStyles.first(where: {$0.code == code})?.prompt ?? "" - - }.store(in: &cancellables) - - $figureDescriptionContent.sink {[weak self] str in - self?.descriptionTextView.defaultText = str - }.store(in: &cancellables) - - $figureDescriptionAIGenerated.sink {[weak self] str in - self?.descriptionTextView.defaultText = str - self?.figureDescriptionContent = str - }.store(in: &cancellables) - - descriptionTextView.textDidChanged = {[weak self] textField in - self?.figureDescriptionContent = textField.text.trimmed - } - - Publishers.CombineLatest($figureDescriptionContent, $selectStyleId).map{ - content, selectStyleCode in - if content.trimmed.count > 0 && selectStyleCode.count > 0{ - return true - } - return false - }.assign(to: &$buttonEnable) - - WalletCore.shared.$balance.sink {[weak self] balance in - if let priceLabel = self?.coinLabel { - priceLabel.contentLabel.text = balance.displayBalance() - } - }.store(in: &cancellables) - -//#if DEBUG -//figureDescriptionContent = "一个来自古代神话的森林守护者,外形优雅而神秘,皮肤如同古树树皮般带有天然纹理,身上点缀着会发光的藤蔓与花朵。双眸宛如月光湖水,闪烁着宁静的光辉,长发由柔和的苔藓与羽毛交织而成,随风轻盈飘动。她身披由自然能量编织的长袍,衣角化作轻盈的雾气,脚下生长出发光的花草。周身萦绕着漂浮的光点与透明的灵体生物,场景被微光森林与梦幻薄雾笼罩,奇幻插画风格,超高精细,8K 分辨率。" -//#endif - } - - // MARK: - Publish - func scrollSelectedShow(){ - // 将选中的style的cell滑动显示出来 - if let index = self.datas.firstIndex(where: {$0.code == selectStyleId}){ - // print("currentindex : \(index)") - self.styleCv.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: false) - } - } - - // MARK: - Helper - - private func setupButtonVIPBuyStyle(){ - bottomButton.context(size: .large) - let buttonString = "Buy VIP +10/Month" - let aStr = StyleButton.getVIPBlackTitleByWords(words: buttonString)// getAttributeTitleByWords(words: buttonString) - bottomButton.setAttributedTitle(aStr, for: .normal) - } - - private func setupViews() { - bottomView = { - let v = UIView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - make.height.equalTo(80) - } - return v - }() - coinLabel = { - let v = CLIconLabel() - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalTo(24) - } - return v - }() - - bottomButton = { - let v = StyleButton(type: .custom) - //v.context(size: .large) - v.primary(size: .large) - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-24) - //make.leading.equalToSuperview().offset(106) - make.leading.greaterThanOrEqualTo(coinLabel.snp.trailing).offset(16) - } - return v - }() - - - container = { - let v = LTScrollContainer() - v.stack.spacing = 24 - v.stack.alignment = .leading - v.scrollView.delegate = self - addSubview(v) - v.snp.makeConstraints { make in - // make.top.equalTo(navigationView.snp.bottom) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(bottomView.snp.top).offset(0) - } - return v - }() - - titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 16 - v.optionInnerTopPadding = 16 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - container.stack.setCustomSpacing(0, after: titleView) - - styleContainer = { - let v = UIView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - } - return v - }() - - styleTipLabel = { - let v = UILabel() - styleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview().offset(12) - } - v.font = .t.tlm - v.textColor = .c.ctpn - v.text = "Style" - return v - }() - - styleCv = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - let size = CGSize(width: 100, height: 100 + 24) - layout.itemSize = size - layout.minimumLineSpacing = 12 - layout.minimumInteritemSpacing = 12 - layout.sectionInset = UIEdgeInsets(top: 0, left: CGFloat.lrs, bottom: 0, right: CGFloat.lrs) - let v = UICollectionView(frame: .zero, collectionViewLayout: layout) - v.showsHorizontalScrollIndicator = false - styleContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(styleTipLabel.snp.bottom).offset(12) - make.height.equalTo(size.height) - make.bottom.equalToSuperview() - } - v.backgroundColor = .clear - v.register(RoleFigureStyleCell.self, forCellWithReuseIdentifier: "RoleFigureStyleCell") - v.dataSource = self - v.delegate = self - return v - }() - - descriptionTextView = { - let v = TitleTextView() - v.maxLimit = 1000 - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - generate1Button = { - let v = TextButton() - v.text = "自动生成" - descriptionTextView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.centerY.equalTo(descriptionTextView.titleLabel) - make.height.equalTo(40) - } - return v - }() - - referenceLabelsView = { - let v = TitleAndSubTitleView() - container.stack.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - } - return v - }() - - titleView.title = "Create" - - let string = "\(Coin(cents: 7000).thousandthsFormatted) to Create" - let titleAstr = NSAttributedString.getIconTitleAttributeByWords(words: string, iconImage: UIImage.icon32Diamond, iconSize: CGSize(width: 24, height: 24), textFont: .t.tll, textColor: .white) - //let titleAstr = StyleButton.getUnlockAttributeTitleByCoin(coin: 7000, string: "to Create") - - descriptionTextView.titleLabel.text = "Description" - descriptionTextView.placeholder = "请描述形象的肤色、服饰、发型、五官、动作、背景等" - - bottomButton.setAttributedTitle(titleAstr, for: .normal) - referenceLabelsView.titleLabel.text = "Reference" - referenceLabelsView.subLabel.text = "AI will create based on the basic image of the character" - - coinLabel.iconImageView.image = UIImage(named: "icon_16_diamond") - - coinLabel.contentLabel.text = "-" - } -} - -extension ChatBackgroundGenerateInputView: UIScrollViewDelegate{ - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView?.titleLabel) - } -} - -extension ChatBackgroundGenerateInputView:UICollectionViewDataSource, UICollectionViewDelegate{ - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RoleFigureStyleCell", for: indexPath) as! RoleFigureStyleCell - let item = datas[indexPath.item] - cell.setupData(data: item) - cell.setupSelected(selected: item.code == selectStyleId) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let item = datas[indexPath.item] - selectStyleId = item.code ?? "" - selectStylePrompt = item.prompt ?? "" - collectionView.reloadData() - } -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundResultGridCell.swift b/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundResultGridCell.swift deleted file mode 100644 index 01ffd9c..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundResultGridCell.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// ChatBackgroundGenerateGridCell.swift -// Crush -// -// Created by Leon on 2025/9/24. -// - -class ChatBackgroundResultGridCell: RoleCreateAppearanceResultCell{ - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - - } - - private func setupEvent(){ - - } - - // MARK: - Public - -// func config(_ data: AIUserImageQuery?){ -// self.data = data -// -// iv.loadImage(data?.imageUrl) -// //... -// } - - - - func configSelectUrl(_ url: String?){ - guard let theUrl = data?.imageUrl, let select = url, theUrl == select else{ - setupSelected(false) - return - } - - setupSelected(true) - } - - -} diff --git a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundResultGridView.swift b/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundResultGridView.swift deleted file mode 100644 index 892d973..0000000 --- a/crush/Crush/Src/Modules/Role/Photo/RoleChatBackground/View/ChatBackgroundResultGridView.swift +++ /dev/null @@ -1,294 +0,0 @@ -// -// ChatBackgroundResultGridView.swift -// Crush -// -// Created by Leon on 2025/8/17. -// - -import UIKit -import Combine -class ChatBackgroundResultGridView: CLContainer { - var headView: ChatBackgroundGenerateGridHeadView! - var layout = UICollectionViewFlowLayout() - var cv: UICollectionView! - - var backButton: EPIconTertiaryButton! - var bottomButton: StyleButton! - // var titleView: TitleView! - var headHeight: CGFloat = 104 - - var datas: [AIUserImageQuery] = [] - -// @Published var selectImgUrl: String? - @Published var selectImgs = [AIUserImageQuery]() - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .large, iconCode: .arrowLeftBorder) - let size = v.bgImageSize() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - make.size.equalTo(CGSize(width: 88, height: size.height)) - } - return v - }() - - bottomButton = { - let v = StyleButton(type: .custom) - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - // make.leading.equalToSuperview().offset(CGFloat.lrs) - make.leading.equalTo(backButton.snp.trailing).offset(16) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom * 0.5 - 16) - } - return v - }() - - cv = { - let width = floor((UIScreen.width - 24 * 2 - 16) * 0.5) - let height = floor(width * 219.0 / 164.0) - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 12, left: 24, bottom: UIWindow.safeAreaBottom, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(ChatBackgroundResultGridCell.self, forCellWithReuseIdentifier: "ChatBackgroundGenerateGridCell") - cv.register(UICollectionReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: "UICollectionReusableView") - addSubview(cv) - cv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - return cv - }() - - headView = { - let v = ChatBackgroundGenerateGridHeadView() - return v - }() - - //titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: titleView.preCalculateHeight()) - bottomButton.setTitle("Confirm", for: .normal) - } - - private func setupData() { - } - - private func setupEvent() { - $selectImgs.sink {[weak self] imgs in - self?.bottomButton.isEnabled = imgs.count > 0 - }.store(in: &cancellables) - } - - // MARK: Public - public func config(_ datas: [AIUserImageQuery]?) { - let validDatas = datas ?? [] - if validDatas.count >= 1{ - self.datas = validDatas - }else{ - self.datas.removeAll(where: {$0.status != .completed}) - // 如果datas的数量不够6个,补齐datas的数量为6个,补充AIUserImageQuery() - if self.datas.count < 6{ - for _ in 0...5 - self.datas.count { - self.datas.append(AIUserImageQuery()) - } - } - } - - // Reselect - var tempSelectImages = [AIUserImageQuery]() - for perNew in validDatas { - for selectImg in selectImgs { - if selectImg.imageUrl == perNew.imageUrl{ - tempSelectImages.append(perNew) - } - } - } - selectImgs = tempSelectImages - - cv.reloadData() - } - - public func setupLastGenerateProcessStart() { - headView.generateButton.isEnabled = false - } - - public func setupLastGenerateProcessEnd() { - headView.generateButton.isEnabled = true - } - -} - -extension ChatBackgroundResultGridView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ChatBackgroundGenerateGridCell", for: indexPath) as! ChatBackgroundResultGridCell - let data = datas[indexPath.item] - cell.config(data: data) - - let isSelected = selectImgs.contains(where: { $0 === data }) - cell.setupSelected(isSelected) - - cell.tapFullBrowseAction = {[weak self] data, image in - guard let self = self else{return} - guard let url = data.imageUrl, !url.isEmpty else { return } - - var photoModels = [PhotoBrowserModel]() - - var startIndex = 0 - for (index, per) in self.datas.enumerated() { - let model = PhotoBrowserModel() - model.image = image - model.sourceRect = cell.screenRect ?? .zero - model.imageUrl = per.imageUrl - if let imageUrl = per.imageUrl, imageUrl == url{ - startIndex = index - } - photoModels.append(model) - } - - ImageBrowser.show(models: photoModels, index: startIndex, type: .chatBackgroundGeneratedSelect) - } - - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let data = datas[indexPath.item] - - guard data.status == .completed else{ - return - } - - if selectImgs.contains(where: { $0 === data }) { - selectImgs.removeAll { $0 === data } - } else { - // 加这行表示单选, 不加表示多选⚠️ - selectImgs.removeAll() - selectImgs.append(data) - } - collectionView.reloadData() - selectImgs = selectImgs - } - - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { - let header = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: "UICollectionReusableView", - for: indexPath - ) - if headView.superview == nil || headView.superview != header { - headView.removeFromSuperview() - } - - header.addSubview(headView) - headView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - // make.bottom.equalToSuperview() - } - - return header - } - return UICollectionReusableView() - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - return CGSize(width: UIScreen.width, height: headHeight) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, alphaViews: [navigationView?.titleLabel]) - } -} - -class ChatBackgroundGenerateGridHeadView: UIView { - var titleView: TitleView! - var titleLabel: CLLabel! - var generateButton: TextButton! - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - titleView = { - let v = TitleView() - addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = CLLabel() - v.font = .t.tlm - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalTo(titleView.snp.bottom).offset(12) - make.bottom.equalToSuperview().offset(-12) - } - return v - }() - - generateButton = { - let v = TextButton() - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-24) - make.centerY.equalTo(titleLabel) - } - return v - }() - - titleLabel.text = "Appearance" - titleView.title = "Confirm the work" - generateButton.setTitle("Regenerate", for: .normal) - } - - private func setupData() { - } - - private func setupEvent() { - } -} - diff --git a/crush/Crush/Src/Modules/Role/Voice/Model/VoiceModels.swift b/crush/Crush/Src/Modules/Role/Voice/Model/VoiceModels.swift deleted file mode 100644 index c24335a..0000000 --- a/crush/Crush/Src/Modules/Role/Voice/Model/VoiceModels.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// VoiceModels.swift -// Crush -// -// Created by Leon on 2025/7/30. -// - -import Foundation - -class VoiceModel { - - - var voiceDict:TimbreDict - - /// 音高 - var dialoguePitch: String = "0" - /// 语速 - var dialogueSpeechRate: String = "25" - - var speechModel: SpeechModel? - - init(voiceDict: TimbreDict, speechModel: SpeechModel? = nil) { - self.voiceDict = voiceDict - - if let pitchRate = voiceDict.pitchRate{ - dialoguePitch = "\(pitchRate)" - } - if let speechRate = voiceDict.speechRate{ - dialogueSpeechRate = "\(speechRate)" - } - - if speechModel != nil{ - self.speechModel = speechModel - }else if let url = voiceDict.url{ - self.speechModel = SpeechModel.modelWith(path: url) - } - } - - func syncConfigFrom(other: VoiceModel?){ - guard let otherVoice = other else{return} - - dialoguePitch = otherVoice.dialoguePitch - dialogueSpeechRate = otherVoice.dialogueSpeechRate -// voiceDict = otherVoice.voiceDict -// speechModel = otherVoice.speechModel - } - -} diff --git a/crush/Crush/Src/Modules/Role/Voice/RoleVoiceListController.swift b/crush/Crush/Src/Modules/Role/Voice/RoleVoiceListController.swift deleted file mode 100644 index 26fb59f..0000000 --- a/crush/Crush/Src/Modules/Role/Voice/RoleVoiceListController.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// RoleVoiceListController.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import JXPagingView -import UIKit -class RoleVoiceListController: CLBaseTableController { - var selectVoiceId : Int? - // var selectVoice: VoiceModel? - - var voices = [VoiceModel]() - - - var selectVoiceAction: ((VoiceModel?) -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - tableView.contentInset = .init(top: 20, left: 0, bottom: UIWindow.safeAreaBottom, right: 0) - tableView.register(RoleVoiceListCell.self, forCellReuseIdentifier: "RoleVoiceListCell") - } - - private func setupDatas() { - select(voiceId: selectVoiceId) - } - - private func setupEvents() { - } - - override func loadData() { - - } - - public func select(voiceId: Int?) { - guard let selectId = voiceId else{return} - selectVoiceId = selectId - -// for per in voices { -// let voice = per -// if voice.voiceDict.id! == selectId { -// selectVoice = voice -// } -// } - if tableView != nil{ - tableView.reloadData() - } - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return voices.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "RoleVoiceListCell", for: indexPath) as! RoleVoiceListCell - cell.delegate = self - let model = voices[indexPath.row] - cell.config(model: model) - - if let selectId = selectVoiceId, selectId == model.voiceDict.id! { - cell.setupSelectedState(selected: true) - } else { - cell.setupSelectedState(selected: false) - } - - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // None? - let model = voices[indexPath.row] - if SpeechManager.checkVoiceState(), let speechModel = model.speechModel{ - SpeechManager.shared.startPlay(with: speechModel) - } - } -} - -extension RoleVoiceListController: RoleVoiceListCellDelegate { - func roleVoiceListTapAdd(_ cell: RoleVoiceListCell, model: VoiceModel?) { - guard let voice = model else { - return - } - - // 已经选中,点击x - if let selectId = selectVoiceId, selectId == voice.voiceDict.id! { - selectVoiceId = nil - selectVoiceAction?(nil) - tableView.reloadData() - }else{ - selectVoiceId = voice.voiceDict.id - selectVoiceAction?(voice) - } - - - } - - func roleVoiceListTapPlay(_ cell: RoleVoiceListCell, model: VoiceModel?) { - // see tableView didSelectRowAt -// print("roleVoiceListTapPlay") -// -// if SpeechManager.checkVoiceState(), let speechModel = model!.speechModel{ -// SpeechManager.shared.startPlay(with: speechModel) -// } - } -} - -extension RoleVoiceListController: JXPagingViewListViewDelegate { - func listView() -> UIView { - return view - } - - func listScrollView() -> UIScrollView { - return tableView - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - listViewDidScrollCallback = callback - } -} diff --git a/crush/Crush/Src/Modules/Role/Voice/RoleVoiceSetController.swift b/crush/Crush/Src/Modules/Role/Voice/RoleVoiceSetController.swift deleted file mode 100644 index 521be47..0000000 --- a/crush/Crush/Src/Modules/Role/Voice/RoleVoiceSetController.swift +++ /dev/null @@ -1,395 +0,0 @@ -// -// RoleVoiceSetController.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import JXPagingView -import JXSegmentedView -import UIKit -import Combine - -class RoleVoiceSetController: CLBaseViewController { - weak var viewModel: RoleCreateViewModel! - - /// Public 外部传入, 上次选中的声音 - var selectVoiceBefore: VoiceModel? - /// Public 外部传入, 首次进入,需要根据之前的性别选择,默认选中。 - var sexSelect: Sex? - - private let segmentedViewHeight = 40 - - private lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - private lazy var pagingView = JXPagingListRefreshView(delegate: self) - private var headerView: RoleVoiceSetHeaderView! - private var headerViewHeight: CGFloat = 288 - - private var controllers = [JXPagingViewListViewDelegate]() - private let dataSource = JXSegmentedTitleDataSource() - - private var maleVoiceListVc: RoleVoiceListController! - private var femaleVoiceListVc: RoleVoiceListController! - - var selectVoiceAction: ((_ voice: VoiceModel?) -> Void)? - // Data - @Published private var selectVoice:VoiceModel? - private var cancellables = Set() - - // Flag - var tabSelectIndexLast: Int = 0 - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - navigationController?.interactivePopGestureRecognizer?.isEnabled = false - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - SpeechManager.shared.stopPlayCurrent() - navigationController?.interactivePopGestureRecognizer?.isEnabled = true - } - - private func setupViews() { - navigationView.alpha0Title = "Voice" - navigationView.styleMainButton.text = "Confirm" - navigationView.styleMainButton.addTarget(self, action: #selector(tapNaviComfirm), for: .touchUpInside) - headerView = { - let v = RoleVoiceSetHeaderView() - v.heightChangeBlock = { [weak self] height in - self?.headerViewHeight = height - self?.pagingView.resizeTableHeaderViewHeight() - } - return v - }() - - pagingView.mainTableView.backgroundColor = .clear - view.addSubview(pagingView) - pagingView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - - dataSource.clNormalStyle() - dataSource.titles = ["Male", "Female"] - - segmentedView.listContainer = pagingView.listContainerView - segmentedView.dataSource = dataSource - segmentedView.delegate = self - segmentedView.clNormalStyle() - - maleVoiceListVc = { - let vc = RoleVoiceListController() - controllers.append(vc) - return vc - }() - - femaleVoiceListVc = { - let vc = RoleVoiceListController() - controllers.append(vc) - return vc - }() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - disabledFullScreenPan() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - enabledFullScreenPan() - } - - private func setupDatas() { - - guard let timbreDictArray = AppDictManager.shared.aiDict?.timbreDictList else{ - Hud.showIndicator() - AppDictManager.shared.loadAIDict {[weak self] ok in - Hud.hideIndicator() - if let array = AppDictManager.shared.aiDict?.timbreDictList, array.count > 0{ - self?.setupDatas() - } - } - return - } - - // 分男女 - var maleVoices = [VoiceModel]() - var femaleVoices = [VoiceModel]() - - // 上次选中的 - var selectVoiceLastTimeId = 0 - if let lastTime = selectVoiceBefore{ - selectVoiceLastTimeId = lastTime.voiceDict.id! - } - var defaultTabIndex = 0 - let sex = sexSelect ?? .noncomfirming - - for (_, per) in timbreDictArray.enumerated(){ - // 男 - if let type = per.type, type == 1{ - let model = VoiceModel(voiceDict: per) - if maleVoices.count <= 0 && sex == .male{ - defaultTabIndex = 0 - } - if per.id! == selectVoiceLastTimeId{ - selectVoice = model - selectVoice?.syncConfigFrom(other: selectVoiceBefore) - defaultTabIndex = 0 - } - - maleVoices.append(model) - }else{ - // 女 - let model = VoiceModel(voiceDict: per) - if femaleVoices.count <= 0 && sex == .female{ - defaultTabIndex = 1 - } - if per.id! == selectVoiceLastTimeId{ - selectVoice = model - selectVoice?.syncConfigFrom(other: selectVoiceBefore) - defaultTabIndex = 1 - } - femaleVoices.append(model) - } - } - - tabSelectIndexLast = defaultTabIndex - segmentedView.defaultSelectedIndex = defaultTabIndex - - maleVoiceListVc.voices = maleVoices - femaleVoiceListVc.voices = femaleVoices - - } - - private func setupEvents() { - headerView.roleVoiceAdjustView.tapCloseButtonBlock = {[weak self] in - self?.selectVoice = nil - } - - maleVoiceListVc.selectVoiceAction = {[weak self] result in - dlog("🎤当前选中的音频:\(String(describing: result))") - self?.selectVoice = result - } - - femaleVoiceListVc.selectVoiceAction = {[weak self] result in - dlog("🎤当前选中的音频2:\(String(describing: result))") - -// self?.headerView.bind(voice: result) -// self?.refreshViewStates() - - self?.selectVoice = result - - } - - $selectVoice.sink {[weak self] result in - guard let `self` = self else { - return - } - - SpeechManager.shared.stopPlayCurrent() - - self.headerView.bind(voice: result) - self.refreshViewStates() - - assert(self.femaleVoiceListVc != nil, "femaleVoiceListVc must be initialized") - if let voice = result{ - self.femaleVoiceListVc.select(voiceId: voice.voiceDict.id!) - self.maleVoiceListVc.select(voiceId: voice.voiceDict.id!) - }else{ - self.femaleVoiceListVc.select(voiceId: 0) - self.maleVoiceListVc.select(voiceId: 0) - } - }.store(in: &cancellables) - } - - // MARK: - Helper - - private func refreshViewStates(){ -// dlog("两个voice:\(String(describing: maleVoiceListVc.selectVoice)) and \(String(describing: femaleVoiceListVc.selectVoice))") -// navigationView.styleMainButton.isEnabled = (maleVoiceListVc.selectVoice != nil || femaleVoiceListVc.selectVoice != nil) - dlog("headView's voice \(String(describing: headerView.roleVoiceAdjustView.voice))") - navigationView.styleMainButton.isEnabled = headerView.roleVoiceAdjustView.voice != nil - } - - // MARK: - Action - - @objc private func tapNaviComfirm(){ - guard selectVoice != nil else {return} - - let pitch = "\(headerView.roleVoiceAdjustView.speechLoudness)" - let speed = "\(headerView.roleVoiceAdjustView.speechRate)" - selectVoice?.dialoguePitch = pitch - selectVoice?.dialogueSpeechRate = speed - - selectVoiceAction?(selectVoice) - navigationController?.popViewController(animated: true) - } - - - -} - -extension RoleVoiceSetController: JXPagingViewDelegate, JXSegmentedViewDelegate { - func tableHeaderViewHeight(in _: JXPagingView) -> Int { - return Int(headerViewHeight) - } - - func tableHeaderView(in _: JXPagingView) -> UIView { - return headerView - } - - func heightForPinSectionHeader(in _: JXPagingView) -> Int { - return segmentedViewHeight - } - - func viewForPinSectionHeader(in _: JXPagingView) -> UIView { - return segmentedView - } - - func numberOfLists(in _: JXPagingView) -> Int { - return controllers.count - } - - func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { - let vc = controllers[index] - return vc - } - - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { - if tabSelectIndexLast != index{ - tabSelectIndexLast = index; - SpeechManager.shared.stopPlayCurrent() - } - } - - func mainTableViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} - -class RoleVoiceSetHeaderView: UIView { - var voiceRecommentTitle: UILabel! - var voiceSelectView: VoiceSelectView! - var roleVoiceAdjustView: RoleVoiceAdjustView! - - var heightChangeBlock: ((CGFloat) -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - let titleView = { - let v = TitleView() - v.optionInnerBottomPadding = 16 - v.optionInnerTopPadding = 16 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - titleView.titleLabel.text = "Voice" - - let subTitleView = { - let v = UILabel() - v.font = .t.tlm - v.textColor = .c.ctsn - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalTo(titleView.snp.bottom).offset(12) - } - v.text = "Select your voice" - return v - }() - - // StackViewV has voiceSelectView, RoleVoiceAdjustView - let stackView = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 12 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalTo(subTitleView.snp.bottom).offset(12) - } - return v - }() - - voiceSelectView = { - let v = VoiceSelectView() - stackView.addArrangedSubview(v) - v.snp.makeConstraints { _ in - // make.height.equalTo(120) - } - return v - }() - - roleVoiceAdjustView = { - let v = RoleVoiceAdjustView() - stackView.addArrangedSubview(v) - v.snp.makeConstraints { _ in - // make.height.equalTo(120) - } - return v - }() - - voiceSelectView.isHidden = false - roleVoiceAdjustView.isHidden = true - - voiceRecommentTitle = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .c.ctpn - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.top.equalTo(stackView.snp.bottom).offset(12) - } - v.text = "Voice Recommendation" - return v - }() - } - - override func layoutSubviews() { - super.layoutSubviews() - let maxY = voiceRecommentTitle.frame.maxY + 12 - heightChangeBlock?(maxY) - } - - public func bind(voice: VoiceModel?){ - if voice != nil{ - voiceSelectView.isHidden = true - roleVoiceAdjustView.isHidden = false - - let name = voice?.voiceDict.name ?? "-" - roleVoiceAdjustView.titleLabel.text = name - - }else{ - voiceSelectView.isHidden = false - roleVoiceAdjustView.isHidden = true - } - roleVoiceAdjustView.bind(voice: voice) - } -} diff --git a/crush/Crush/Src/Modules/Role/Voice/View/RoleVoiceAdjustView.swift b/crush/Crush/Src/Modules/Role/Voice/View/RoleVoiceAdjustView.swift deleted file mode 100644 index 1db4ed0..0000000 --- a/crush/Crush/Src/Modules/Role/Voice/View/RoleVoiceAdjustView.swift +++ /dev/null @@ -1,357 +0,0 @@ -// -// RoleVoiceAdjustView.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import UIKit -import Combine -class RoleVoiceAdjustView: UIView { - var titleLabel: UILabel! - var closeButton: EPIconTertiaryButton! - - var toneTipLabel: UILabel! - var toneSlider: CustomSlider! - - var speedTipLabel: UILabel! - var speedSlider: CustomSlider! - - var bottomStackH: UIStackView! - var playButton: AudioButton! - var playTipLabel: UILabel! - var playTopButton : UIButton! - -// var tapPlayButtonBlock: (() -> Void)? - - var tapCloseButtonBlock: (() -> Void)? - - // Data - - var voice: VoiceModel? - - var speechModel : SpeechModel? - /// 音高 - var speechLoudness: Int = 0 // 改为0,对应中间值 - /// 语速 - var speechRate: Int = 25 - - /// 音高 - var audioNowSpeechPitch : Int? - /// 语速 - var audioNowSpeechSpeed : Int? - - -// var adjustVoice: Bool = false - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = UIColor.c.csbn - layer.cornerRadius = 16 - layer.masksToBounds = true - - titleLabel = { - let v = UILabel() - v.textColor = .c.ctpn - v.font = .t.tlm - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(18) - make.leading.equalToSuperview().offset(16) - } - return v - }() - - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .xs, iconCode: .delete) - v.addTarget(self, action: #selector(tapCloseButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(18) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - toneTipLabel = { - let v = UILabel() - v.textColor = .c.ctsn - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalTo(titleLabel.snp.bottom).offset(18) - } - v.text = "Low" - - let high = { - let v = UILabel() - v.textColor = .c.ctsn - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(titleLabel.snp.bottom).offset(18) - } - v.text = "High" - return v - }() - high.isHidden = false - - return v - }() - - toneSlider = { - let v = CustomSlider() - addSubview(v) - v.numberOfSteps = 11 - v.selectedStep = 5 - v.translatesAutoresizingMaskIntoConstraints = false - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - //make.top.equalTo(titleLabel.snp.bottom).offset(42) - make.top.equalTo(toneTipLabel.snp.bottom).offset(4) - make.height.equalTo(44) - } - return v - }() - - speedTipLabel = { - let v = UILabel() - v.textColor = .c.ctsn - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.top.equalTo(toneSlider.snp.bottom).offset(18) - } - v.text = "Slow" - - let high = { - let v = UILabel() - v.textColor = .c.ctsn - v.font = .t.tls - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(toneSlider.snp.bottom).offset(18) - } - v.text = "Fast" - return v - }() - high.isHidden = false - - return v - }() - - speedSlider = { - let v = CustomSlider() - addSubview(v) - v.numberOfSteps = 11 - v.selectedStep = 5 - v.translatesAutoresizingMaskIntoConstraints = false - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(speedTipLabel.snp.bottom).offset(4) - make.height.equalTo(44) - } - return v - }() - - bottomStackH = { - let v = UIStackView() - v.axis = .horizontal - v.spacing = 12 - v.alignment = .center - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(224) - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-16) - make.height.equalTo(40) - } - return v - }() - - playButton = { - let v = AudioButton(style: .voiceThemePlay, size: .square24) - v.addTarget(self, action: #selector(tapPlayButton), for: .touchUpInside) - bottomStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(16) - } - return v - }() - - playTipLabel = { - let v = UILabel() - v.font = .t.tll - v.textColor = .c.cpvn - bottomStackH.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(24) - } - return v - }() - - playTopButton = { - let v = UIButton() - addSubview(v) - v.addTarget(self, action: #selector(tapPlayButton), for: .touchUpInside) - v.snp.makeConstraints { make in - make.edges.equalTo(bottomStackH) - } - return v - }() - - speedSlider.$selectedStep.sink {[weak self] step in - - // 11 steps[0~10], -50 to 100 - let speed = -50 + step * 15 - self?.speechRate = speed - dlog("speed step:\(step), speed:\(speed)") - }.store(in: &cancellables) - - toneSlider.$selectedStep.sink {[weak self] step in - - // 11 steps[0~10], -12 to 12 - let loudness = -12 + Float(step) * 2.4 - let loudnessInt = Int(round(loudness)) - self?.speechLoudness = loudnessInt - dlog("pitch step:\(step), loundess:\(loudnessInt)") - }.store(in: &cancellables) - - // titleLabel.text = "Young Male Voice" - playTipLabel.text = "Tap to play" - } - - // MARK: - Public - public func bind(voice: VoiceModel?){ - if let newVoice = voice, let voiceType = newVoice.voiceDict.voiceType{ - if let beforeVoiceType = self.voice?.voiceDict.voiceType, voiceType != beforeVoiceType{ - dlog("select new type voice.") - speechModel = nil - } - - self.voice = newVoice - // 兼容老数据:-50~100 映射到 -12~12 - if let pitch = voice?.dialoguePitch, let pitchInt = Int(pitch){ - // 老数据范围:-50~100,新数据范围:-12~12 - var normalizedPitch: Int - if pitchInt > 12 { - normalizedPitch = 12 - } else if pitchInt < -12{ - normalizedPitch = -12 - } - else { - // 新数据直接使用 - normalizedPitch = pitchInt - } - - // 将音高值映射到滑块步骤 - let step = Int(round((Double(normalizedPitch) - (-12)) / 2.4)) - let clampedStep = max(0, min(10, step)) - toneSlider.selectedStep = clampedStep - } - - if let speed = voice?.dialogueSpeechRate, let speedInt = Int(speed){ - // get closest step - let step = Int((speedInt + 50) / 15) - speedSlider.selectedStep = step - } - } - - - } - - - // MARK: - Helper - - func prepareAudio(content: String?, completion:((_ model: SpeechModel?)-> Void)?){ -// #warning("test") -// playButton.startIndicator() -// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { -// completion?(SpeechModel()) -// } -// return - - guard let text = content, text.count > 0, let voiceType = voice?.voiceDict.voiceType else{ - return - } - - guard AudioPlayTool.audioChannelFreeToUse() else{ - dlog("Audio Channel 不可用") - return - } - - let saywords = String.removeBracketContents(from: text) - guard saywords.count > 0 else{ - return - } - - let speechRate = self.speechRate - let loudnessRate = self.speechLoudness - - var params = [String: Any]() - params.updateValue(saywords, forKey: "text") - params.updateValue(voiceType, forKey: "voiceType") - params.updateValue(speechRate, forKey: "speechRate") - params.updateValue(loudnessRate, forKey: "pitchRate") - - playButton.startIndicator() - AICowProvider.request(.voiceTts(params: params), modelType: String.self) {[weak self] result in - switch result { - case .success(let model): - if let audioString = model{ - self?.speechModel = SpeechModel.modelWithBase64String(audioString) - completion?(self?.speechModel) - } - case .failure: - Hud.hideIndicator() - completion?(SpeechModel()) - } - } - } - - // MARK: - Action - @objc func tapPlayButton() { - dlog("play audio, speechRate: \(speechRate), speechLoudness: \(speechLoudness)") - - if let speechModel = self.speechModel, let pitch = audioNowSpeechPitch, let speed = audioNowSpeechSpeed, pitch == speechRate, speed == speechLoudness { - playButton.reloadState(with: speechModel) - SpeechManager.shared.startPlay(with: speechModel) - }else{ - speechModel = nil - let voiceText = voice?.voiceDict.voiceText ?? "我是来自北方的一匹孤星的狼" - prepareAudio (content: voiceText) {[weak self] model in - if let speechModel = model{ - self?.audioNowSpeechPitch = self?.speechLoudness - self?.audioNowSpeechSpeed = self?.speechRate - - self?.playButton.reloadState(with: speechModel) - SpeechManager.shared.startPlay(with: speechModel) - } - else{ - // self?.playButton.reloadState(with: nil) - assert(false, "cannot return nil") - } - } - } - } - - @objc func tapCloseButton() { - tapCloseButtonBlock?() - } -} diff --git a/crush/Crush/Src/Modules/Role/Voice/View/RoleVoiceListViews.swift b/crush/Crush/Src/Modules/Role/Voice/View/RoleVoiceListViews.swift deleted file mode 100644 index b4c556b..0000000 --- a/crush/Crush/Src/Modules/Role/Voice/View/RoleVoiceListViews.swift +++ /dev/null @@ -1,138 +0,0 @@ -import UIKit - -protocol RoleVoiceListCellDelegate: AnyObject { - func roleVoiceListTapAdd(_ cell: RoleVoiceListCell, model: VoiceModel?) - func roleVoiceListTapPlay(_ cell: RoleVoiceListCell, model: VoiceModel?) -} - -class RoleVoiceListCell: UITableViewCell { - var block: UIView! - var playButton: AudioButton!//EPIconGhostButton! - var titleLabel: UILabel! - var subTitleLabel : UILabel! - var selectMark: EPIconGhostButton! - - var data: VoiceModel? - weak var delegate: RoleVoiceListCellDelegate? - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .clear - selectionStyle = .none - - block = { - let v = UIView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview() - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-16) - } - v.backgroundColor = .c.csbn - v.layer.cornerRadius = 16 - v.clipsToBounds = true - return v - }() - - playButton = { - let v = AudioButton(style: .clearBg) - //v.addTarget(self, action: #selector(tapVoicePlay(sender:)), for: .touchUpInside) - v.isUserInteractionEnabled = false - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(16) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.numberOfLines = 0 - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(52) - make.trailing.equalToSuperview().offset(-64) - make.top.equalToSuperview().offset(16) - } - v.font = .t.tbm - v.textColor = .c.ctpn - return v - }() - - subTitleLabel = { - let v = UILabel() - block.addSubview(v) - v.numberOfLines = 0 - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(52) - make.top.equalTo(titleLabel.snp.bottom).offset(4) - make.bottom.equalToSuperview().offset(-16) - } - v.font = .t.tbsn - v.textColor = .c.ctsn - return v - }() - - - selectMark = { - let v = EPIconGhostButton(radius: .none, iconSize: .small, iconCode: .add) - v.addTarget(self, action: #selector(tapVoiceAdd(sender:)), for: .touchUpInside) - block.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - make.size.equalTo(v.bgImageSize()) - } - return v - }() -// #warning("test") -// testData() - } - - private func testData(){ - titleLabel.text = "ABC DEF Voice" - subTitleLabel.text = "ABC DEF Voice" - } - - public func config(model: VoiceModel){ - data = model - - titleLabel.text = model.voiceDict.name - subTitleLabel.text = model.voiceDict.description//"ABC DEF Voice" - - playButton.reloadState(with: model.speechModel) - } - - public func setupSelectedState(selected: Bool){ - if selected{ - block.layer.borderWidth = 2 - block.layer.borderColor = UIColor.c.cpn.cgColor - selectMark.iconCode = .delete - selectMark.optionalIconColor = UIColor.c.cpn - }else{ - block.layer.borderWidth = 0 - selectMark.iconCode = .add - selectMark.optionalIconColor = UIColor.c.ctpn - } - } - - // MARK: - Action - -// @objc private func tapVoicePlay(sender: UIButton){ -// delegate?.roleVoiceListTapPlay(self, model: self.data) -// } - - @objc private func tapVoiceAdd(sender: UIButton){ - delegate?.roleVoiceListTapAdd(self, model: self.data) - } -} diff --git a/crush/Crush/Src/Modules/Role/Voice/View/RoleVoicePlayButton.swift b/crush/Crush/Src/Modules/Role/Voice/View/RoleVoicePlayButton.swift deleted file mode 100644 index a78f970..0000000 --- a/crush/Crush/Src/Modules/Role/Voice/View/RoleVoicePlayButton.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// RoleVoicePlayButton.swift -// Crush -// -// Created by Leon on 2025/9/6. -// - -import UIKit - -class RoleVoicePlayButton: UIView{ - -} diff --git a/crush/Crush/Src/Modules/Tab/TabBarController.swift b/crush/Crush/Src/Modules/Tab/TabBarController.swift deleted file mode 100755 index 4f0eab4..0000000 --- a/crush/Crush/Src/Modules/Tab/TabBarController.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// TableViewController.swift -// Tabbar -// -// Created by lym on 2020/12/31. -// - -import Alamofire -import UIKit -import Combine -class TabBarController: UITabBarController { - private var customTabBar = TabBar() - - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .c.cbd - configViewControllers() - setupEvent() - } - - private func setupEvent() { - - - NotificationCenter.default.addObserver(self, selector: #selector(notiPresentSignInVc), name: AppNotificationName.presentSignInVc.notificationName, object: nil) -// NotificationCenter.default.addObserver(self, selector: #selector(unreadChanged), name: AppNotificationName.unreadCountChanged.notificationName, object: nil) - - IMManager.shared.$chatTabAllUnreadCount.sink {[weak self] count in - guard let `self` = self else { - return - } - guard let chatItem = self.customTabBar.tabItems?[1] else { return } - chatItem.count = count - }.store(in: &cancellables) - } - - deinit { - NotificationCenter.default.removeObserver(self) - print("♻️TabBarController") - } -} - -extension TabBarController { - public func setSelected(index: TabBarItemIndex) { - customTabBar.setSelected(selected: true, index: index) - switch index { - case .home: - selectedIndex = 0 - case .friend: - selectedIndex = 1 - case .discover: - selectedIndex = 2 - case .me: - selectedIndex = 3 - case .add: - break - } - } -} - -extension TabBarController { - private func configViewControllers() { - let home = CLNavigationController(rootViewController: HomePageRootController()) - let friend = CLNavigationController(rootViewController: FriendsRootHomeController()) - let discover = CLNavigationController(rootViewController: DiscoverRootPageController()) - let me = CLNavigationController(rootViewController: MeRootPageController()) - viewControllers = [home, friend, discover, me] - - let homeItem = TitleItem(title: "1", - image: UIImage(named: "tabbar_foryou")!, - selectedImage: UIImage(named: "tabbar_foryou_selected")!, - itemIndex: .home) - homeItem.count = 0 - - let friendItem = TitleItem(title: "2", - image: UIImage(named: "tabbar_contact")!, - selectedImage: UIImage(named: "tabbar_contact_selected")!, - itemIndex: .friend) - friendItem.count = 0 - - let discoverItem = TitleItem(title: "3", - image: UIImage(named: "tabbar_explore")!, - selectedImage: UIImage(named: "tabbar_explore_selected")!, - itemIndex: .discover) - - let meItem = TitleItem(title: "4", - image: UIImage(named: "tabbar_me")!, - selectedImage: UIImage(named: "tabbar_me_selected")!, - itemIndex: .me) - meItem.count = 0 - - let addItem = TitleItem(title: "+", - image: UIImage(named: "tabbar_create_app")!, - selectedImage: UIImage(named: "tabbar_create_app")!, - itemIndex: .add) - - let childItem: [TabbarItem] = [homeItem, friendItem, addItem, discoverItem, meItem] - customTabBar.tabItems = childItem - customTabBar.tabBarDelegate = self - setValue(customTabBar, forKey: "tabBar") - } - - // MARK: noti - - @objc private func notiPresentSignInVc() { - if UserCore.shared.isLogin() { - return - } - - if let vc = UIWindow.getTopViewController(), vc.isKind(of: CLLoginMainController.self) { - dlog("⚠️已弹出登录") - } else { - let vc = CLLoginMainController() - let navc = CLNavigationController(rootViewController: vc) - navc.modalPresentationStyle = .fullScreen - AppRouter.shared.isBlockBackRootVc = true - present(navc, animated: true) { - AppRouter.shared.isBlockBackRootVc = false - } - } - } - -// @objc private func unreadChanged() { -// guard let chatItem = customTabBar.tabItems?[1] else { return } -// chatItem.count = IMManager.shared.chatTabAllUnreadCount -// } -} - -// MARK: - TabBarDelegate - -extension TabBarController: TabBarDelegate { - func tabBar(_: TabBar, didSelectItemAt index: TabBarItemIndex) { - switch index { - case .home: - #if DEBUG - if APIConfig.environment != .appStore && APIConfig.environment != .product{ - if selectedIndex == 0 { - UIWindow.getTopViewController()?.navigationController?.pushViewController(TestEntrancesController(), animated: true) - } - } - #endif - selectedIndex = 0 - case .friend: - selectedIndex = 1 - case .discover: - selectedIndex = 2 - case .me: - selectedIndex = 3 - case .add: - break - //AppRouter.goCreateEditAIRole() - } - - IMManager.shared.regetAllUnreadCount() - } - - func tabBar(_: TabBar, canTapEnterItem index: TabBarItemIndex) -> Bool { - switch index { - case .me, .friend: - if UserCore.shared.checkUserLoginIfNotPushUserToLogin() { - return true - } else { - return false - } - case .add: - if UserCore.shared.checkUserLoginIfNotPushUserToLogin(){ - AppRouter.goCreateEditAIRole() - } - return false - default: - return true - } - } -} diff --git a/crush/Crush/Src/Modules/Tab/View/TabBar.swift b/crush/Crush/Src/Modules/Tab/View/TabBar.swift deleted file mode 100755 index d80d47f..0000000 --- a/crush/Crush/Src/Modules/Tab/View/TabBar.swift +++ /dev/null @@ -1,278 +0,0 @@ -// -// TabBar.swift -// Tabbar -// -// Created by lym on 2021/1/1. -// - -import Lottie -import UIKit - -protocol TabBarDelegate: AnyObject { - func tabBar(_ tabBar: TabBar, didSelectItemAt index: TabBarItemIndex) - func tabBar(_ tabBar: TabBar, canTapEnterItem index: TabBarItemIndex) -> Bool -} - -class TabBar: UITabBar { - weak var tabBarDelegate: TabBarDelegate? - - var tabItems: [TabbarItem]? { - didSet { - guard let items = tabItems else { - return - } - containerStack.removeSubviews() - for (index, item) in items.enumerated() { - containerStack.addArrangedSubview(item) - item.addTarget(self, action: #selector(clickItem), for: .touchUpInside) - item.selectedStatus = index == 0 - } - } - } - - static let containerHeight: CGFloat = 52 - private var containerInsets = UIEdgeInsets(top: 20, left: 24, bottom: 24, right: 24) - - private var container: UIView = { - let view = UIView() - view.backgroundColor = .clear // clear - return view - }() - - private var containerStack: UIStackView = { - let stack = UIStackView() - stack.axis = .horizontal - stack.distribution = .fillEqually - stack.spacing = 0 - return stack - }() - - private lazy var bgView: UIView = { - let view = UIView() - view.backgroundColor = .c.csdn - return view - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - if #available(iOS 13.0, *) { - let appearance = self.standardAppearance.copy() - appearance.backgroundImage = UIColor.clear.toImage() ?? UIImage() - appearance.shadowImage = UIColor.clear.toImage() ?? UIImage() - appearance.shadowColor = .clear - appearance.configureWithTransparentBackground() - self.standardAppearance = appearance - } else { - backgroundImage = UIColor.clear.toImage() ?? UIImage() - shadowImage = UIColor.clear.toImage() ?? UIImage() - isTranslucent = true - barTintColor = .clear - } - - backgroundColor = .c.cbd - - addSubview(bgView) - bgView.addSubview(container) - container.layer.cornerRadius = TabBar.containerHeight / 2 - container.addSubview(containerStack) - bringSubviewToFront(bgView) - - bgView.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets.zero) - } - - let bottom = safeAreaInsets.bottom > 0 ? safeAreaInsets.bottom : containerInsets.bottom - container.snp.makeConstraints { make in - make.left.equalToSuperview().offset(16)//(containerInsets.left) - make.right.equalToSuperview().offset(-16)//(-containerInsets.right) - make.height.equalTo(TabBar.containerHeight) - make.bottom.equalToSuperview().offset(-bottom) - make.bottom.equalToSuperview() - } - - containerStack.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(UIEdgeInsets.zero) - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if clipsToBounds || alpha == 0 || isHidden { - return nil - } - for item in containerStack.arrangedSubviews.reversed() { - let subPoint = item.convert(point, from: self) - let imgPoint = (item as! TitleItem).imageView.convert(point, from: self) - let flag = item.point(inside: subPoint, with: event) - let flag2 = item.point(inside: imgPoint, with: event) - if flag || flag2 { - return item - } - } - return super.hitTest(point, with: event) - } - -// override func layoutSubviews() { -// super.layoutSubviews() -// } -} - -extension TabBar { - public func setSelected(selected _: Bool, index: TabBarItemIndex) { - guard let items = tabItems else { return } - items.forEach { item in - if item.itemIndex == index { - item.selectedStatus = true - item.starAnimation() - } else { - item.selectedStatus = false - item.stopAnimation() - } - } - } - - public func setCount(count: Int, index: TabBarItemIndex) { - guard let items = tabItems else { return } - items.forEach { item in - if item.itemIndex == index { - item.count = count - } - } - } -} - -extension TabBar { - @objc private func clickItem(_ sender: TitleItem) { - switch sender.itemIndex { - case .friend, .me, .discover, .home, .add: - if let can = tabBarDelegate?.tabBar(self, canTapEnterItem: sender.itemIndex), can == true { - setSelected(selected: true, index: sender.itemIndex) - tabBarDelegate?.tabBar(self, didSelectItemAt: sender.itemIndex) - } - // case .add: - // tabBarDelegate?.tabBar(self, didSelectItemAt: sender.itemIndex) - // } - } - } -} - -// MARK: - TabBar item - -enum TabBarItemIndex { - case home - case friend - /// 额外的添加按钮 - case add - case discover - case me - -} - -enum TitleItemStyle { - case small - case large -} - -protocol TabbarItem: UIControl { - var selectedStatus: Bool { get set } - var count: Int { get set } - var itemIndex: TabBarItemIndex { get set } - func starAnimation() - func stopAnimation() -} - -extension TabbarItem { - func starAnimation() {} - func stopAnimation() {} -} - -class TitleItem: UIControl, TabbarItem { - private var titleLabel: UILabel! - //private var bage: PaddingLabel! - private var badgeView: BadgeView! -// private var lotiView: AnimationView! - - var imageView: UIImageView! - var image: UIImage! - var selectedImage: UIImage! - var style: TitleItemStyle! - var font = UIFont.t.tll // UIFont = .popSemiBold(size: 12) - var selectedFont: UIFont = .t.tll // .popSemiBold(size: 12) - var textColor: UIColor = .c.ctsn - var selectedTextColor: UIColor = .white // .hexEF7000 - - public var title: String { - didSet { - titleLabel.text = title - } - } - - public var count: Int = 0 { - didSet { - badgeView.isHidden = count <= 0 - badgeView.badgeValue = count// > 99 ? "99+" : "\(count)" - } - } - - public var selectedStatus: Bool = false { - didSet { - if selectedStatus { - imageView.image = selectedImage - } else { - imageView.image = image - } - } - } - - public var itemIndex: TabBarItemIndex - - init(title: String, image: UIImage, - selectedImage: UIImage, - count: Int = 0, - style: TitleItemStyle = .small, - itemIndex: TabBarItemIndex) { - self.title = title - self.count = count - self.image = image - self.selectedImage = selectedImage - self.style = style - self.itemIndex = itemIndex - super.init(frame: CGRect.zero) - setUpUI() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setUpUI() { - backgroundColor = .clear - - imageView = UIImageView() - imageView.image = image - imageView.contentMode = .scaleAspectFit - addSubview(imageView) - imageView.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.centerY.equalToSuperview() - } - - badgeView = { - let v = BadgeView() - v.showMax99 = true - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(imageView.snp.trailing).offset(-8) - make.top.equalTo(imageView).offset(-4) - } - return v - }() - - } -} diff --git a/crush/Crush/Src/Modules/TestEntrances/Data/DatabaseTestViewController.swift b/crush/Crush/Src/Modules/TestEntrances/Data/DatabaseTestViewController.swift deleted file mode 100644 index 7b74770..0000000 --- a/crush/Crush/Src/Modules/TestEntrances/Data/DatabaseTestViewController.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// DatabaseTestViewController.swift -// Crush -// -// Created by Leon on 2025/7/15. -// - -import UIKit - -class DatabaseTestViewController: CLBaseViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - - } - - -} diff --git a/crush/Crush/Src/Modules/TestEntrances/Data/TestDatasTableController.swift b/crush/Crush/Src/Modules/TestEntrances/Data/TestDatasTableController.swift deleted file mode 100644 index 45faf34..0000000 --- a/crush/Crush/Src/Modules/TestEntrances/Data/TestDatasTableController.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// TestDatasTableController.swift -// Crush -// -// Created by Leon on 2025/7/15. -// - -import Foundation -import UIKit - -// MARK: - 配置模型 - -struct TableViewConfig { - // 数据模型 - struct Item { - let title: String - let subtitle: String? - let action: () -> Void // 点击单元格执行的方法 - } - - let items: [Item] // 数据源 - let cellIdentifier: String // 单元格重用标识 - let rowHeight: CGFloat // 行高 - let configureCell: (UITableViewCell, Item) -> Void // 配置单元格的闭包 -} - -// MARK: - 通用 TableView 数据源和代理 - -class TestDatasTableHandler: NSObject, UITableViewDataSource, UITableViewDelegate { - private let config: TableViewConfig - - init(config: TableViewConfig) { - self.config = config - } - - // MARK: - UITableViewDataSource - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return config.items.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: config.cellIdentifier, for: indexPath) - let item = config.items[indexPath.row] - config.configureCell(cell, item) - return cell - } - - // MARK: - UITableViewDelegate - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return config.rowHeight - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let item = config.items[indexPath.row] - item.action() - } -} - -// MARK: - TestDatasTableController - -class TestDatasTableController: CLBaseViewController { - private var tableView: UITableView = UITableView() - private var handler: TestDatasTableHandler! - - override func viewDidLoad() { - super.viewDidLoad() - title = "Test Datas" - setupTableView() - } - - private func setupTableView() { - view.addSubview(tableView) - - tableView.snp.makeConstraints { make in - make.top.equalTo(navigationView.snp.bottom) - make.leading.trailing.bottom.equalToSuperview() -// make.top.equalToSuperview() - } - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") - - let config = TableViewConfig( - items: [ - .init(title: "Print iconfont", subtitle: "xxx", action: { [weak self] in self?.printIconFontEnum() }), - .init(title: "Action 1", subtitle: "Execute first method", action: { [weak self] in self?.performAction1() }), - .init(title: "Action 2", subtitle: "Execute second method", action: { [weak self] in self?.performAction2() }), - .init(title: "Action 3", subtitle: nil, action: { [weak self] in self?.performAction3() }), - ], - cellIdentifier: "Cell", - rowHeight: 44.0, - configureCell: { cell, item in - cell.textLabel?.text = item.title - cell.detailTextLabel?.text = item.subtitle - } - ) - - handler = TestDatasTableHandler(config: config) - tableView.dataSource = handler - tableView.delegate = handler - } - - private func printIconFontEnum() { - MWIconFont.printIconCodeEnum() - } - - private func performAction1() { - print("Performed Action 1") - } - - private func performAction2() { - print("Performed Action 2") - } - - private func performAction3() { - print("Performed Action 3") - } -} diff --git a/crush/Crush/Src/Modules/TestEntrances/TestEntrancesController.swift b/crush/Crush/Src/Modules/TestEntrances/TestEntrancesController.swift deleted file mode 100644 index 59b4096..0000000 --- a/crush/Crush/Src/Modules/TestEntrances/TestEntrancesController.swift +++ /dev/null @@ -1,516 +0,0 @@ -// -// TestEntrancesController.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import UIKit -import Combine -import URLNavigator -class TestEntrance { - let title: String - let destination: UIViewController.Type? - let category: String - let action: (() -> Void)? - - init(title: String, destination: UIViewController.Type? = UIViewController.self, action:(() -> Void)? = nil, category: String) { - self.title = title - self.destination = destination - self.category = category - self.action = action - } -} - -class TestEntrancesController: CLBaseViewController { - private let tableView = UITableView(frame: .zero, style: .grouped) - private var sections: [String] = [] - private var testItems: [String: [TestEntrance]] = [:] - - var photoModels: [PhotoBrowserModel] = [PhotoBrowserModel]() - - lazy var recordTool = AudioRecordTool() - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - configureTestItems() - } - - private func setupUI() { - //view.backgroundColor = .white - title = "Test Entrances" - - // Setup TableView - tableView.backgroundColor = .clear - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.delegate = self - tableView.dataSource = self - tableView.register(TestEntranceCell.self, forCellReuseIdentifier: TestEntranceCell.reuseIdentifier) - view.addSubview(tableView) - tableView.snp.makeConstraints { make in - //make.edges.equalToSuperview() - make.leading.trailing.bottom.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - - let headView = TestEntrancesHeadView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: 48)) - tableView.tableHeaderView = headView - } - - - private func configureTestItems() { - let common = "Common" - let auth = "Auth" - let role = "Role" - let data = "Data" - let chat = "Chat" - let user = "User" - let discover = "Discover" - let network = "Network" - let ui = "UI" - - let customOrder = [common,ui, auth, role, data,chat, user,discover, network] - - // Sample test configurations - let items = [ - TestEntrance(title: "TapCommon01", action: {[weak self] in self?.tapCommon01()}, category: common), - TestEntrance(title: "TapCommon02", action: {[weak self] in self?.tapCommon02()}, category: common), - TestEntrance(title: "TapCommon03", action: {[weak self] in self?.tapCommon03()}, category: common), - TestEntrance(title: "TapCommon04", action: {[weak self] in self?.tapCommon04()}, category: common), - TestEntrance(title: "TapCommon05 Data", action: {[weak self] in self?.tapCommon05()}, category: common), - TestEntrance(title: "Audio Record and upload", action: {[weak self] in self?.tapCommon06()}, category: common), - TestEntrance(title: "TapCommon07", action: {[weak self] in self?.tapCommon07()}, category: common), - - TestEntrance(title: "UI Components", destination: UITestViewController.self, category: ui), - TestEntrance(title: "TestUI1Controller", destination: TestUI1Controller.self, category: ui), - - TestEntrance(title: "Login Test", destination: CLLoginMainController.self, category: auth), - - TestEntrance(title: "Role Classification", destination: RoleClassificationSelectController.self, category: role), - TestEntrance(title: "Role Voice", destination: RoleVoiceSetController.self, category: role), - TestEntrance(title: "Role Figure Generate", destination: RoleFigureGenerateController.self, category: role), - TestEntrance(title: "Role Home Page", destination: RoleHomePagerController.self, category: role), - TestEntrance(title: "Role Photo Generate", destination: RolePhotoGenerateController.self, category: role), - - - TestEntrance(title: "Database Test", destination: DatabaseTestViewController.self, category: data), - TestEntrance(title: "IAP Test", destination: DatabaseTestViewController.self, category: data), - TestEntrance(title: "Data Tool", destination: TestDatasTableController.self, category: data), - - TestEntrance(title: "Chat Setting", destination: ChatSettingListController.self, category: chat), - TestEntrance(title: "Phone Calling", destination: PhoneCallController.self, category: chat), - - - TestEntrance(title: "Profile Test", destination: ProfileTestViewController.self, category: user), - TestEntrance(title: "Setting", destination: SettingListController.self, category: user), - - TestEntrance(title: "Meet Drag test", destination: MeetDragCardExampleViewController.self, category: discover), - - - TestEntrance(title: "Network Test", destination: NetworkTestViewController.self, category: network), - - - ] - - // Group items by category - testItems = Dictionary(grouping: items, by: { $0.category }) - - // Get all categories in their original order - let allCategories = items.map { $0.category }.uniqued() - - // Sort categories: prioritize customOrder, then append remaining categories in original order - sections = customOrder + allCategories.filter { !customOrder.contains($0) } - - tableView.reloadData() - } -} - -extension TestEntrancesController{ - // Functions - private func tapCommon01(){ -// let vc = UnlockPriceSetDialogController() -// vc.show() - -// Alert.showAIRoleCreateSuccessAlert() -// let photo = UploadPhotoM() -// photo.image = UIImage(named: "egpic") -// photo.addThisItemTimeStamp = Date().timeStamp -// -// Hud.showIndicator() -// CloudStorage.shared.s3BatchAddPhotos([photo], bucket: .HEAD_IMAGE) { result in -// Hud.hideInidcator() -// dlog("✅测试上传图片:\(result)") -// } - -// let color = CLGlobalToken.color(token: .glo_color_violet_10) -// dlog("当前颜色:\(color)") - -// let vc = ChatModePickSheet() -// vc.show() - - - } - - private func tapCommon02(){ - let sheet = BuyCreditsSheetView() - sheet.show() - } - private func tapCommon03(){ - let cropViewController = CropViewController(image: UIImage(named: "egpic")!) { [unowned self] croppedImage in - dlog("crop image: \(croppedImage)") - } - let navc = CLNavigationController(rootViewController: cropViewController) - UIWindow.getTopViewController()?.present(navc, animated: true, completion: nil) - } - private func tapCommon04(){ - //var photoModels: [EGPhotoBrowserModel] = [EGPhotoBrowserModel]() - photoModels.removeAll() - do{ - let model = PhotoBrowserModel() -// model.image = UIImage(named: "egpic") - model.imageUrl = "https://img2.baidu.com/it/u=3634539496,393899961&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=563" - photoModels.append(model) - } - - - do{ - let model = PhotoBrowserModel() -// model.image = UIImage(named: "egpic") - model.imageUrl = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.nga.178.com%2Fattachments%2Fmon_202011%2F28%2F7nQ5-gg1iZ2hT3cS16p-23w.jpg&refer=http%3A%2F%2Fimg.nga.178.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1756093859&t=f7aa3f1da2fcbda3e44de75aaa5f224a" - photoModels.append(model) - } - ImageBrowser.show(models: photoModels, index: 0, type: .roleMine) - } - - private func tapCommon05(){ - let mockAIUserRequest = AIUserRequest( - aiId: 123, - nickname: "MockAI", - sex: .female, - headImg: "https://example.com/avatar.jpg", - birthday: "2000-01-01", - roleCode: "mentor", - characterCode: "kind", - tagCode: "funny", - introduction: "I'm a mock AI used for testing.", - permission: 1, - imageUrl: "https://example.com/image.jpg", - imageWidth: 720, - imageHeight: 1280, - aiUserExt: AIUserRequestExt( - profile: "Deep personality profile here", - dialogueStyle: "Friendly", - dialoguePrologue: "Hey there! Nice to meet you.", - dialogueTimbreCode: "warm", - dialoguePitch: "medium", - dialogueSpeechRate: "normal", - imageStyleCode: "realistic", - imageDesc: "A cheerful character in a futuristic setting", - imageReferenceUrl: "https://example.com/reference.jpg" - ) - ) - - let jsonString:String = CodableHelper.encodeToJSONString(mockAIUserRequest)! - dlog("AIUserRequest: \(jsonString)") - - let model = CodableHelper.decode(AIUserRequest.self, from: jsonString) - dlog("model: \(String(describing: model))") - } - - private func tapCommon06(){ - if recordTool.audioRecorder?.isRecording ?? false{ - dlog("audio结束录制") - recordTool.stopRecord() - }else{ - dlog("audio开始录制") - recordTool.startRecord { path in - if let pathInSandbox = path { - do { - - let data = try Data(contentsOf: pathInSandbox) - // ✅ data 就是文件的二进制数据 - let model = UploadModel() - model.addThisItemTimeStamp = Date().timeStamp - model.fileData = data - CloudStorage.shared.s3AddUploadAudio(model) { result in - dlog("Audio upload结果:\(result)") - } - } catch { - print("❌读取文件audio mp3失败: \(error)") - } - - } - } - } - } - - private func tapCommon07(){ -// AICowProvider.request(.voiceAsr2(url: "https://snd.crushlevel.ai/dev/main/sound/439213911113729/1756776603015.mp3"), modelType: EmptyModel.self) { result in -// switch result { -// case .success(let model): -// dlog(model) -// case .failure: -// break -// } -// } - -// let v = CoinsRechargeSheet() -// v.show() -// CLPurchase.shared.showIAPBuyCoinSheet() -// Hud.showIndicator() - - } -} - -extension Sequence where Element: Hashable { - func uniqued() -> [Element] { - var seen = Set() - return filter { seen.insert($0).inserted } - } -} - -// MARK: - UITableViewDataSource - -extension TestEntrancesController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return sections.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let category = sections[section] - return testItems[category]?.count ?? 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: TestEntranceCell.reuseIdentifier, for: indexPath) as! TestEntranceCell - let category = sections[indexPath.section] - if let item = testItems[category]?[indexPath.row] { - cell.configure(with: item) - } - return cell - } - - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return sections[section] - } -} - -// MARK: - UITableViewDelegate - -extension TestEntrancesController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let category = sections[indexPath.section] - if let item = testItems[category]?[indexPath.row] { - // item.destination is UIViewController.Type - guard let type = item.destination else{ - fatalError() - } - - if let action = item.action{ - action() - return - } - - if let baseVCType = type as? CLBaseViewController.Type { - let shouldShow = baseVCType.shouldPresentThisVc - if shouldShow { - let destinationVC = type.init() - let navc = CLNavigationController(rootViewController: destinationVC) - navc.modalPresentationStyle = .fullScreen - present(navc, animated: true) - return - } - } - let destinationVC = type.init() - navigationController?.pushViewController(destinationVC, animated: true) - } - } -} - -// MARK: - Custom Cell - -class TestEntranceCell: UITableViewCell { - static let reuseIdentifier = "TestEntranceCell" - - private let titleLabel = UILabel() - private let categoryLabel = UILabel() - private let arrowLabel = UILabel() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupUI() { - backgroundView?.backgroundColor = .clear - backgroundColor = .clear - contentView.backgroundColor = .clear - - // Configure labels - titleLabel.font = .systemFont(ofSize: 16, weight: .medium) - titleLabel.textColor = .white - - categoryLabel.font = .systemFont(ofSize: 14, weight: .regular) - categoryLabel.textColor = .gray - - arrowLabel.text = "→" - arrowLabel.textColor = .white - arrowLabel.font = .systemFont(ofSize: 16) - - // Setup stack views for three-column layout - let stackView = UIStackView(arrangedSubviews: [categoryLabel, titleLabel, arrowLabel]) - stackView.axis = .horizontal - stackView.spacing = 16 - stackView.distribution = .fill - stackView.alignment = .center - stackView.translatesAutoresizingMaskIntoConstraints = false - - contentView.addSubview(stackView) - - // Constraints - NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), - stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), - stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), - stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), - - categoryLabel.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.3), - titleLabel.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.6), - ]) - } - - func configure(with entrance: TestEntrance) { - titleLabel.text = entrance.title - categoryLabel.text = entrance.category - } -} - -// MARK: - Sample Test View Controllers - -class LoginTestViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - title = "Login Test" - } -} - -class ProfileTestViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - title = "Profile Test" - } -} - -class NetworkTestViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - title = "Network Test" - } -} - -class UITestViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - title = "UI Components Test" - } -} - -class TestEntrancesHeadView: UIView{ - var textFiled: CLTextField! - var operateButton : StyleButton! - - private var cancellables = Set() - override init(frame: CGRect) { - super.init(frame: frame) - - operateButton = { - let v = StyleButton() - v.primary(size: .small) - v.addTarget(self, action: #selector(tapOperateButton), for: .touchUpInside) - addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-16) - make.centerY.equalToSuperview() - } - v.isEnabled = false - return v - }() - - textFiled = { - let v = CLTextField() - v.placeholder = "aiId" - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.centerY.equalToSuperview() - make.trailing.equalTo(operateButton.snp.leading).offset(-16) - } - return v - }() - - operateButton.setTitle("进AI主页", for: .normal) - - textFiled.textPublisher.sink {[weak self] text in - guard let str = text else{ - self?.operateButton.isEnabled = false - return - } - self?.operateButton.isEnabled = str.count > 10 - }.store(in: &cancellables) - - // 从缓存中加载之前存储的aiId - loadCachedAIId() - } - - private func setupData(){ - textFiled.text = "444190968774657" - textFiled.sendTextChangedNoti() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - 缓存相关方法 - - /// 从缓存中加载aiId - private func loadCachedAIId() { - if let cachedAIId = AppCache.fetchCache(key: CacheKey.testRecordAIId.rawValue, type: Int.self) { - textFiled.text = "\(cachedAIId)" - textFiled.sendTextChangedNoti() - dlog("✅ 从缓存加载aiId: \(cachedAIId)") - } - } - - /// 将aiId存储到缓存 - private func saveAIIdToCache(_ aiId: Int) { - AppCache.cache(key: CacheKey.testRecordAIId.rawValue, value: aiId) - dlog("✅ 已存储aiId到缓存: \(aiId)") - } - - @objc private func tapOperateButton(){ - guard let text = textFiled.text, let num = Int(text) else{ - return - } - - guard text.count > 10 else{ - return - } - - // 存储aiId到缓存 - saveAIIdToCache(num) - - // 437416915828737 - AppRouter.goAIRoleHome(aiId: num) - } -} diff --git a/crush/Crush/Src/Modules/TestEntrances/TestFunctionsController.swift b/crush/Crush/Src/Modules/TestEntrances/TestFunctionsController.swift deleted file mode 100644 index d66735a..0000000 --- a/crush/Crush/Src/Modules/TestEntrances/TestFunctionsController.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// TestFunctionsController.swift -// Crush -// -// Created by Leon on 2025/7/13. -// - -import Foundation -import UIKit - -class TestFunctionsController:CLViewController{ - -} - - -class TestFunctionsView:UIView{ - -} diff --git a/crush/Crush/Src/Modules/TestEntrances/UI/TestUI1Controller.swift b/crush/Crush/Src/Modules/TestEntrances/UI/TestUI1Controller.swift deleted file mode 100644 index f5e4dd7..0000000 --- a/crush/Crush/Src/Modules/TestEntrances/UI/TestUI1Controller.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// TestUI1Controller.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import UIKit -import SnapKit -import Kingfisher -import APNGKit - -class TestUI1Controller: CLBaseViewController { - - lazy var view1 = UIView() - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(view1) - view1.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalToSuperview().offset(100) - make.size.equalTo(CGSize(width: 100, height: 40)) - } - view1.backgroundColor = .c.cwvn - //.cl.cph - - do{ - let indicatorView = { - let v = APNGImageView() - - v.contentMode = .scaleAspectFit - view.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 96, height: 96)) - } - - if let url = Bundle.main.url(forResource: "generating", withExtension: "png") { - let image = try? APNGImage(fileURL: url) - v.image = image - } - - - - return v - }() - } - } - - -} diff --git a/crush/Crush/Src/Modules/VIP/VIPPrivilegesShowController.swift b/crush/Crush/Src/Modules/VIP/VIPPrivilegesShowController.swift deleted file mode 100644 index 5471ad1..0000000 --- a/crush/Crush/Src/Modules/VIP/VIPPrivilegesShowController.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// VIPPrivilegesShowController.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -import UIKit - -class VIPPrivilegesShowController: CLBaseTableController { - var headView: VIPPrivilegesHeadView! - var bottomButton: StyleButton! - - var data: MemberDetailOutput? - - var privileges = [MemberPrivDict]() - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - let title = "Crushlevel VIP" - navigationView.bgView.alpha = 0 - navigationView.alpha0Title = title - view.bringSubviewToFront(navigationView) - - bottomButton = { - let v = StyleButton() - v.primary(size: .large) - v.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-16 - UIWindow.safeAreaBottom * 0.5) - } - return v - }() - - tableView.snp.remakeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalTo(bottomButton.snp.top).offset(-16) - } - - tableView.register(VIPPrivilegesListCell.self, forCellReuseIdentifier: "VIPPrivilegesListCell") - tableView.bounces = false - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 16 + UIWindow.safeAreaBottom, right: 0) - headView = { - let v = VIPPrivilegesHeadView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: 124 + UIWindow.navBarTotalHeight)) - return v - }() - - tableView.tableHeaderView = headView - - bottomButton.setTitle("Subscribe", for: .normal) - } - - private func setupDatas() { - loadVIPInfo() - loadVIPTiers() - } - - private func loadVIPInfo(){ - Hud.showIndicator() - WalletProvider.request(.vipDetail, modelType: MemberDetailOutput.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - self?.data = model - self?.privileges = model?.memberPrivList ?? [] - self?.handleVIPInfoOutput(output: model) - self?.tableView.reloadData() - case .failure: - break - } - } - } - - private func handleVIPInfoOutput(output: MemberDetailOutput?){ - guard let vipInfo = output?.userMemberInfo else { - return - } - - guard let memberType = vipInfo.memberType else{ - return - } - - headView.config(info: vipInfo) - - if vipInfo.getVIPValid(), memberType == .vip{ - bottomButton.isHidden = true - tableView.snp.remakeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalToSuperview() - } - } - } - - private func loadVIPTiers(){ - CLPurchase.shared.loadVIPTiersProducts { products in - - } - } - - private func setupEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(notifyVIPChanged(_:)), name: AppNotificationName.vipStateChange.notificationName, object: nil) - } - - - @objc private func notifyVIPChanged(_ noti: Notification){ - loadVIPInfo() - } - - - // MARK: - Action - - @objc private func tapBottomButton() { - let sheet = VIPSubscribeSheet() - sheet.show() - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return privileges.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "VIPPrivilegesListCell", for: indexPath) as! VIPPrivilegesListCell - let data = privileges[indexPath.row] - cell.config(data) - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - print("didSelectRowAt \(indexPath.row)") - } - - override func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, alphaViews: [navigationView.titleLabel, navigationView.bgView]) - } -} diff --git a/crush/Crush/Src/Modules/VIP/View/VIPPrivilegesViews.swift b/crush/Crush/Src/Modules/VIP/View/VIPPrivilegesViews.swift deleted file mode 100644 index 16f8580..0000000 --- a/crush/Crush/Src/Modules/VIP/View/VIPPrivilegesViews.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// VIPPrivilegesViews.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -class VIPPrivilegesListCell: UITableViewCell { - var block: UIView! - var gradientBorderContainer: GradientBorderView! - - var icon: UIImageView! - var contentStackV: UIStackView! - var titleLabel: LineSpaceLabel! - var descLabel: LineSpaceLabel! - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalToSuperview().offset(16) - } - return v - }() - - gradientBorderContainer = { - let gradient = CLSystemToken.gradient(token: .ccvn) - - let v = GradientBorderView(colors: gradient.colors(), gradientType: .leftToRight) - v.gBorderWidth = 1 - v.layer.cornerRadius = 8 - v.layer.masksToBounds = true - v.backgroundColor = .c.cbdi - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.size.equalTo(CGSize(width: 160, height: 107)) - make.top.greaterThanOrEqualToSuperview().offset(0) - make.bottom.lessThanOrEqualToSuperview() - } - return v - }() - - icon = { - let v = UIImageView() - v.cornerRadius = 8 -// block.addSubview(v) -// v.snp.makeConstraints { make in -// make.leading.equalToSuperview().offset(24) -// make.size.equalTo(CGSize(width: 160, height: 107)) -// make.top.greaterThanOrEqualToSuperview().offset(0) -// make.bottom.lessThanOrEqualToSuperview() -// } - gradientBorderContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - contentStackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 4 - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(gradientBorderContainer.snp.trailing).offset(16) - make.trailing.equalToSuperview().offset(-24) - make.top.greaterThanOrEqualToSuperview() - make.bottom.lessThanOrEqualToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tts) - v.config(typo) - v.textColor = .text - contentStackV.addArrangedSubview(v) - return v - }() - - descLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tbs) - v.config(typo) - v.textColor = .c.ctsn - contentStackV.addArrangedSubview(v) - return v - }() - -// testData() - } - - private func testData() { - icon.backgroundColor = .random - titleLabel.text = "Virtual character X5" - descLabel.text = "Add 4 virtual role creation opportunities" - } - - func config(_ data: MemberPrivDict?){ - guard let privilege = data else {return} - icon.loadImage(privilege.img, bgColor: .clear) - titleLabel.text = privilege.title - descLabel.text = privilege.desc - } -} - -class VIPPrivilegesHeadView: UIView { - var gradientBg: GradientView! - var titleLabel: CLLabel! - var descLabel: LineSpaceLabel! - - var vipIcon: UIImageView! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - gradientBg = { - let gradient = CLSystemToken.gradient(token: .ccvn) - let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight) - addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - vipIcon = { - let v = UIImageView() - v.image = UIImage(named: "icon_vip_crown") - addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 100, height: 100)) - make.trailing.equalToSuperview().offset(-24) - make.bottom.equalToSuperview().offset(-24) - } - return v - }() - - titleLabel = { - let v = CLLabel() - v.font = .t.tdl - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalToSuperview().offset(UIWindow.navBarTotalHeight) - } - return v - }() - - descLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tls) - v.config(typo) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalTo(titleLabel.snp.bottom).offset(4) - make.trailing.equalTo(vipIcon.snp.leading).offset(-16) - } - return v - }() - - titleLabel.text = "Crushlevel VIP" - descLabel.text = "Only $5.99/month, enjoy more benefits" - } - - func config(info: UserSubscription?){ - guard let vip = info else {return} - - if vip.getVIPValid(), let expTime = vip.expTime{ - let dateString = Date.timerStyle(style: .DMYHM, millisecond: expTime) - descLabel.text = "VIP expired in \(dateString)" - } - } -} diff --git a/crush/Crush/Src/Modules/VIP/View/VIPSubscribeSheet.swift b/crush/Crush/Src/Modules/VIP/View/VIPSubscribeSheet.swift deleted file mode 100644 index 352c523..0000000 --- a/crush/Crush/Src/Modules/VIP/View/VIPSubscribeSheet.swift +++ /dev/null @@ -1,760 +0,0 @@ -// -// VIPSubscribeSheet.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -class VIPSubscribeSheet: EGPopBaseView { - var bgImageView: AutoRatioImageView! - - var titleLabel: UILabel! - var closeButton: EPIconTertiaryButton! - - var privilegesContainer: UIView! - var effectView: UIVisualEffectView! - var layout = UICollectionViewFlowLayout() - var cv: UICollectionView! - var leftButton: EPIconTertiaryDarkButton! - var rightButton: EPIconTertiaryDarkButton! - - var agreenmentContainer: UIView! - - var operateButton: StyleButton! - - var tableView: UITableView! - - // MARK: - Data Properties - private var vipProducts: [VIPSubcribeProduct] = [] - private var privileges: [MemberPrivDict] = [] - private var currentRealCvIndex = 0 - /// 0~10 - private var currentPrivilegeIndex = 0 - private var selectedProductIndex = 0 - private var multiplier = 100 // 用于无限循环的倍数 - private var initialScrollCompleted = false // 标记是否已完成初始滚动 - - init() { - super.init(direction: .bottom) - - contentView.backgroundColor = .c.csbn - contentLength = 693 + UIWindow.safeAreaBottom - - setupViews() - setupData() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Data - private func setupData(){ - - // Group load Hud - Hud.showIndicator() - - let group = DispatchGroup() - group.enter() - CLPurchase.shared.loadVIPTiersProducts { [weak self] products in - group.leave() - // 配置tableView中,充值档位 - if let products = products { - self?.vipProducts = products - DispatchQueue.main.async { - self?.tableView.reloadData() - } - } - } - - group.enter() - CLPurchase.shared.loadCurrentUserVipDetail { [weak self] output in - group.leave() - // get output.memberPrivList 权限展示列表,并显示在cv中 - if let output = output, let privileges = output.memberPrivList { - self?.privileges = privileges - DispatchQueue.main.async { - self?.cv.reloadData() - self?.updateNavigationButtons() - self?.initialScrollToMiddle() // 初始化时滚动到中间位置 - } - } - } - - group.notify(queue: .main) { - // 所有 enter/leave 成对后,回调执行 - print("All completed") - Hud.hideIndicator() - } - } - - private func setupEvent(){ - NotificationCenter.default.addObserver(self, selector: #selector(notifyVIPChanged(_:)), name: AppNotificationName.vipStateChange.notificationName, object: nil) - } - - @objc private func notifyVIPChanged(_ noti: Notification){ - dismiss() - } - - // MARK: - Functions - private func updateNavigationButtons() { - leftButton.isHidden = privileges.count <= 1 - rightButton.isHidden = privileges.count <= 1 - } - - private func scrollToPrivilege(at index: Int) { - guard index >= 0 && index < privileges.count else { return } - currentPrivilegeIndex = index - - // 计算当前应该滚动到的位置 - let totalItems = privileges.count * multiplier - let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count - let targetIndex = middleStart + index - currentRealCvIndex = targetIndex - let indexPath = IndexPath(item: targetIndex, section: 0) - cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) - } - - private func initialScrollToMiddle() { - guard privileges.count > 0 && !initialScrollCompleted else { return } - let totalItems = privileges.count * multiplier - let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count - currentRealCvIndex = middleStart - let indexPath = IndexPath(item: middleStart, section: 0) - - DispatchQueue.main.async { - self.cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) - self.initialScrollCompleted = true - } - } - - // MARK: - Action - - @objc private func leftButtonPressed() { - guard privileges.count > 0 else { return } - - // 计算下一个目标位置(相邻位置) - let nextRealIndex = currentRealCvIndex - 1 - - // 检查是否需要边界处理 - let totalItems = privileges.count * multiplier - let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count - - if nextRealIndex < 0 { - // 如果超出左边界,跳转到右侧对应位置 - let targetIndex = totalItems - 1 - currentRealCvIndex = targetIndex - currentPrivilegeIndex = targetIndex % privileges.count - let indexPath = IndexPath(item: targetIndex, section: 0) - cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) - } else { - // 正常向左切换 - currentRealCvIndex = nextRealIndex - currentPrivilegeIndex = nextRealIndex % privileges.count - let indexPath = IndexPath(item: nextRealIndex, section: 0) - cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) - } - } - - @objc private func rightButtonPressed() { - guard privileges.count > 0 else { return } - - // 计算下一个目标位置(相邻位置) - let nextRealIndex = currentRealCvIndex + 1 - - // 检查是否需要边界处理 - let totalItems = privileges.count * multiplier - - if nextRealIndex >= totalItems { - // 如果超出右边界,跳转到左侧对应位置 - let targetIndex = 0 - currentRealCvIndex = targetIndex - currentPrivilegeIndex = targetIndex % privileges.count - let indexPath = IndexPath(item: targetIndex, section: 0) - cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) - } else { - // 正常向右切换 - currentRealCvIndex = nextRealIndex - currentPrivilegeIndex = nextRealIndex % privileges.count - let indexPath = IndexPath(item: nextRealIndex, section: 0) - cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) - } - } - - @objc private func termsOfServicePressed() { - AppRouter.goTermsOfService() - dismiss() - } - - @objc private func privacyPolicyPressed() { - AppRouter.goPrivacyPolicy() - dismiss() - } - - @objc private func tapBottomButton() { - // 开始续期订阅... - guard selectedProductIndex < vipProducts.count else { return } - let selectedProduct = vipProducts[selectedProductIndex] - print("选择的产品: \(selectedProduct.productId ?? "")") - - guard let productId = selectedProduct.productId, let payment = selectedProduct.payAmount else{ - return - } - - let product = IAPProducts() - product.productId = productId - product.chargeAmount = payment - - IAPCore.shared.requestProducts([product]) {iapIds in - if iapIds.count > 0 { - let tradeId = String.randomNumber(length: 10) - IAPCore.shared.addPayProductId(productId: productId, tradeId: tradeId) - } - } - - - } - - // MARK: - UI - private func setupViews() { - bgImageView = { - let v = AutoRatioImageView() - v.setImage(UIImage(named: "vip_sheet_bg")) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - } - return v - }() - - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .text - v.textAlignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(24) - } - v.text = "Crushlevel VIP" - return v - }() - - setupPrivilegeView() - - setupAgreementsView() - - operateButton = { - let v = StyleButton() - v.primary(size: .large) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalTo(agreenmentContainer.snp.top).offset(-16) - } - v.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) - v.setTitle("Subscribe", for: .normal) - return v - }() - - setupVIPSubscribeItemsView() - } - - private func setupPrivilegeView() { - privilegesContainer = { - let v = UIView() - v.cornerRadius = 12 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview().offset(64) - make.height.equalTo(229) - } - return v - }() - - effectView = { - let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - v.alpha = 0.9 - privilegesContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - // cv is in privilegesContainer, cellwidth and height is same as privilegesContainer. pageEnable = true, scroll hori.. - cv = { - layout.scrollDirection = .horizontal - let width = UIScreen.width - 24 * 2 - layout.itemSize = CGSize(width: width, height: 229) - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 0 - layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - - let v = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) - v.backgroundColor = .clear - v.register(VIPPrivilegeGridCell.self, forCellWithReuseIdentifier: "VIPPrivilegeGridCell") - v.delegate = self - v.dataSource = self - v.isPagingEnabled = true - v.showsHorizontalScrollIndicator = false - privilegesContainer.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - leftButton = { - let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .xs, iconCode: .arrowLeftBorder) - privilegesContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(58.5) - make.leading.equalToSuperview().offset(16) - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - rightButton = { - let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .xs, iconCode: .arrowRightBorder) - privilegesContainer.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(58.5) - make.trailing.equalToSuperview().offset(-16) - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - leftButton.addTarget(self, action: #selector(leftButtonPressed), for: .touchUpInside) - rightButton.addTarget(self, action: #selector(rightButtonPressed), for: .touchUpInside) - } - - private func setupAgreementsView() { - agreenmentContainer = { - let v = UIView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(32) - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - } - return v - }() - - let line = { - let v = CLLine() - agreenmentContainer.addSubview(v) - v.snp.makeConstraints { make in - make.center.equalToSuperview() - make.size.equalTo(CGSize(width: 1, height: 8)) - } - return v - }() - - let leftButton = { - let v = UIButton() - v.titleLabel?.font = UIFont.t.tls - v.setTitleColor(.c.ctsn, for: .normal) - agreenmentContainer.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalTo(line.snp.leading).offset(-16) - make.top.bottom.equalToSuperview() - } - v.addTarget(self, action: #selector(termsOfServicePressed), for: .touchUpInside) - return v - }() - - let rightButton = { - let v = UIButton() - v.titleLabel?.font = UIFont.t.tls - v.setTitleColor(.c.ctsn, for: .normal) - agreenmentContainer.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(line.snp.trailing).offset(16) - make.top.bottom.equalToSuperview() - } - v.addTarget(self, action: #selector(privacyPolicyPressed), for: .touchUpInside) - return v - }() - - leftButton.setTitle("Terms of Service", for: .normal) - rightButton.setTitle("Privacy Policy", for: .normal) - } - - private func setupVIPSubscribeItemsView() { - tableView = { - let v = UITableView() - v.backgroundColor = .clear - v.register(VIPTierListCell.self, forCellReuseIdentifier: "VIPTierListCell") - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.equalToSuperview() - make.top.equalTo(privilegesContainer.snp.bottom).offset(16) - make.bottom.equalTo(operateButton.snp.top).offset(-16) - } - return v - }() - tableView.delegate = self - tableView.dataSource = self - } - -} - -extension VIPSubscribeSheet: UICollectionViewDelegate, UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return privileges.count * multiplier - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VIPPrivilegeGridCell", for: indexPath) as! VIPPrivilegeGridCell - - let actualIndex = indexPath.item % privileges.count - if actualIndex < privileges.count { - let privilege = privileges[actualIndex] - cell.configure(with: privilege) - } - - return cell - } - - func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - // 更新当前索引 - let pageWidth = scrollView.frame.width - let currentPage = Int(scrollView.contentOffset.x / pageWidth) - currentRealCvIndex = currentPage - currentPrivilegeIndex = currentPage % privileges.count - - // 实现无限循环:当接近边界时,跳转到中间位置 - let totalItems = privileges.count * multiplier - let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count - - if currentPage < privileges.count { - // 接近开始位置,跳转到中间偏后位置 - let targetIndex = middleStart + currentPrivilegeIndex - let targetIndexPath = IndexPath(item: targetIndex, section: 0) - DispatchQueue.main.async { - self.cv.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false) - } - } else if currentPage >= totalItems - privileges.count { - // 接近结束位置,跳转到中间偏前位置 - let targetIndex = middleStart - (privileges.count - currentPrivilegeIndex) - let targetIndexPath = IndexPath(item: targetIndex, section: 0) - DispatchQueue.main.async { - self.cv.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false) - } - } - } -} - -extension VIPSubscribeSheet: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return vipProducts.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "VIPTierListCell", for: indexPath) as! VIPTierListCell - - if indexPath.row < vipProducts.count { - let product = vipProducts[indexPath.row] - cell.configure(with: product) - cell.setupSelected(selected: indexPath.row == selectedProductIndex) - } - - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - print("didSelectRowAt \(indexPath)") - selectedProductIndex = indexPath.row - tableView.reloadData() - } -} - -class VIPPrivilegeGridCell: UICollectionViewCell { - var icon: UIImageView! - var titleLabel: CLLabel! - var descLabel: LineSpaceLabel! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .clear - icon = { - let v = UIImageView() - v.cornerRadius = 8 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 140, height: 93)) - make.top.equalToSuperview().offset(24) - make.centerX.equalToSuperview() - } - return v - }() - - titleLabel = { - let v = CLLabel() - v.font = .t.tts - v.textAlignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(icon.snp.bottom).offset(16) - } - return v - }() - - descLabel = { - let v = LineSpaceLabel() - let typo = CLSystemToken.typography(token: .tls) - v.textColor = .text - v.textAlignment = .center - v.config(typo) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(16) - make.trailing.equalToSuperview().offset(-16) - make.top.equalTo(titleLabel.snp.bottom).offset(8) - } - return v - }() - } - - func configure(with privilege: MemberPrivDict) { - titleLabel.text = privilege.title ?? "" - descLabel.text = privilege.desc ?? "" - - // 设置图片 - if let imgUrl = privilege.img, !imgUrl.isEmpty { - // 使用网络图片加载 - icon.loadImage(imgUrl, bgColor: .clear) - } else { - - } - } -} - -class VIPTierListCell: UITableViewCell { - var blockView: UIView! - var checkButton: EPRadioButton! - var coinAndLabel: CLIconLabel! - - var periodLabel: CLLabel! - var descLabel: CLLabel! - - var gradientFlag: GradientView! - var discountLabelOnFlag: CLLabel! - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - blockView = { - let v = UIView() - v.backgroundColor = .c.csnn - v.cornerRadius = 12 - v.layer.borderWidth = 0 - v.layer.borderColor = UIColor.c.cpn.cgColor - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview() - make.bottom.equalToSuperview().offset(-12) - make.height.equalTo(80) - } - return v - }() - - checkButton = { - let v = EPRadioButton() - v.setupRadioStyle(.checkEmpty) - v.isUserInteractionEnabled = false - blockView.addSubview(v) - v.snp.makeConstraints { make in - make.trailing.equalToSuperview().offset(-24) - make.centerY.equalToSuperview() - } - return v - }() - - coinAndLabel = { - let v = CLIconLabel() - v.iconSize = CGSize(width: 16, height: 16) - //v.iconImageView.image = UIImage(named: "icon_16_diamond") - v.iconImageView.isHidden = true - v.contentLabel.textColor = .text - v.contentLabel.font = .t.tnmm - blockView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalTo(checkButton.snp.leading).offset(-8) - } - return v - }() - - periodLabel = { - let v = CLLabel() - v.font = .t.tts - v.textColor = .text - blockView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(24) - } - return v - }() - - descLabel = { - let v = CLLabel() - v.font = .t.tls - v.textColor = .c.ctsn - blockView.addSubview(v) - v.snp.makeConstraints { make in - make.lastBaseline.equalTo(periodLabel) - make.leading.equalTo(periodLabel.snp.trailing).offset(4) - } - return v - }() - - gradientFlag = { - let gradient = CLSystemToken.gradient(token: .ccvn) - let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight) - v.cornerRadius = 12 - v.backgroundColor = .c.csnn - v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner] - blockView.addSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(24) - make.leading.top.equalToSuperview() - } - return v - }() - - discountLabelOnFlag = { - let v = CLLabel() - v.font = .t.tbss - v.textColor = .c.cbd - gradientFlag.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview().offset(8) - make.trailing.equalToSuperview().offset(-8) - } - return v - }() - } - - func configure(with product: VIPSubcribeProduct) { - // 设置价格 -// if let chargeAmount = product.chargeAmount { -// let coin = Coin(cents: chargeAmount) -// coinAndLabel.contentLabel.text = coin.formatted -// } - - let period = product.period ?? .month - - // 设置周期 - let periodText = getPeriodText(from: product.period) - periodLabel.text = periodText - - // 设置描述价格 - if let payAmount = product.payAmount { - let price = Coin(cents: payAmount) - - if period == .month{ - descLabel.isHidden = true - }else if period == .season{ - descLabel.isHidden = false - let pricePerMonth = CGFloat(payAmount) / 3.0 - let pricePerMonthCoin = Coin(cents: Int(pricePerMonth)) - descLabel.text = "$\(pricePerMonthCoin.formatted)/\(getPeriodShortText(from: product.period))" - }else if period == .year{ - descLabel.isHidden = false - let pricePerMonth = CGFloat(payAmount) / 12.0 - let pricePerMonthCoin = Coin(cents: Int(pricePerMonth)) - descLabel.text = "$\(pricePerMonthCoin.formatted)/\(getPeriodShortText(from: product.period))" - } - // CrushCoin - coinAndLabel.contentLabel.text = "$\(price.formatted)" - } - - // 设置折扣标签 - if let discount = product.discount, let discountFloat = Float(discount), discountFloat > 0 { - gradientFlag.isHidden = false - discountLabelOnFlag.text = "\(discount)% Off" - } else { - gradientFlag.isHidden = true - } - } - - private func getPeriodText(from period: PriceType?) -> String { - switch period { - case .month: - return "1 Month" - case .season: - return "3 Months" - case .year: - return "1 Year" - case .none: - return "1 Month" - } - } - - private func getPeriodShortText(from period: PriceType?) -> String { - switch period { - case .month: - return "Month" - case .season: - return "Months" - case .year: - return "Year" - case .none: - return "Month" - } - } - - func setupSelected(selected: Bool) { - checkButton.isSelected = selected - blockView.layer.borderWidth = selected ? 2 : 0 - } -} diff --git a/crush/Crush/Src/Modules/Wallet/BillDetailListController.swift b/crush/Crush/Src/Modules/Wallet/BillDetailListController.swift deleted file mode 100644 index f8c8635..0000000 --- a/crush/Crush/Src/Modules/Wallet/BillDetailListController.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// BillDetailListController.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -import UIKit - -/// 流水明细 -class BillDetailListController : CLBaseTableController{ - var titleView: TitleView! - - var data: BillListResponse? - var bills = [BillListInfo]() - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - let title = "Transaction Details" - navigationView.alpha0Title = title - - titleView = { - let v = TitleView() - v.title = title - return v - }() - titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: titleView.preCalculateHeight()) - tableView.tableHeaderView = titleView - tableView.register(BillListCell.self, forCellReuseIdentifier: "BillListCell") - tableView.snp.remakeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - - addRefreshHeaderFooter() - } - - private func setupDatas(){ - tableView.mj_header?.beginRefreshing() - } - - override func loadData() { - - var request = BillListRequest() - request.page = RequestPageData() - request.page?.ps = 20 - let thePage = page - request.page?.pn = thePage - request.type = "BALANCE" - - let dict = request.toNonNilDictionary() - - WalletProvider.request(.billList(params: dict), modelType: BillListResponse.self) {[weak self] result in - self?.tableView.mj_header?.endRefreshing() - self?.tableView.mj_footer?.endRefreshing() - - switch result { - case .success(let model): - self?.data = model - let theBills = model?.pageList?.datas ?? [] - if thePage == 1{ - self?.bills = theBills - self?.tableView.reloadData() - self?.view.setupEmpty(empty: theBills.count <= 0, msg: "暂无流水明细") - self?.tableView.mj_footer?.resetNoMoreData() - }else{ - self?.bills.append(contentsOf: theBills) - if theBills.count <= 0 { - self?.tableView.mj_footer?.endRefreshingWithNoMoreData() - } - } - case .failure: - break - } - } - - // "暂无流水明细" - } - - private func setupEvents(){ - - } - - // MARK: - TableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return bills.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "BillListCell", for: indexPath) as! BillListCell - let data = bills[indexPath.row] - cell.config(data) - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - print("didSelectRowAt \(indexPath.row)") - } - - override func scrollViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviTitleAlpha(scrollView: scrollView, titleLabel: navigationView.titleLabel) - } -} diff --git a/crush/Crush/Src/Modules/Wallet/CoinsIncomeTableController.swift b/crush/Crush/Src/Modules/Wallet/CoinsIncomeTableController.swift deleted file mode 100644 index 2d03bf8..0000000 --- a/crush/Crush/Src/Modules/Wallet/CoinsIncomeTableController.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// CoinsIncomeTableController.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -import UIKit -import JXPagingView -class CoinsIncomeTableController: CLBaseTableController { - - var header: CoinsIncomeHeadView! - - var data: BillListResponse? - var bills = [BillListInfo]() - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - header = { - let v = CoinsIncomeHeadView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CoinsIncomeHeadView.heightOfHeader)) - return v - }() - - tableView.tableHeaderView = header - tableView.register(BillListCell.self, forCellReuseIdentifier: "BillListCell") - - addRefreshHeaderFooter() - } - - private func setupDatas(){ - tableView.mj_header?.beginRefreshing() - } - - override func loadData() { - - var request = BillListRequest() - request.page = RequestPageData() - request.page?.ps = 20 - request.type = "INCOME" - let thePage = page - request.page?.pn = thePage - - let dict = request.toNonNilDictionary() - - WalletProvider.request(.billList(params: dict), modelType: BillListResponse.self) {[weak self] result in - self?.tableView.mj_header?.endRefreshing() - self?.tableView.mj_footer?.endRefreshing() - - switch result { - case .success(let model): - self?.data = model - let theBills = model?.pageList?.datas ?? [] - if thePage == 1{ - self?.bills = theBills - self?.tableView.reloadData() - self?.view.setupEmpty(empty: theBills.count <= 0, msg: "暂无流水明细") - self?.tableView.mj_footer?.resetNoMoreData() - }else{ - self?.bills.append(contentsOf: theBills) - if theBills.count <= 0 { - self?.tableView.mj_footer?.endRefreshingWithNoMoreData() - } - } - case .failure: - break - } - } - - } - - private func handleBillResponse(_ model: BillListResponse?){ - - } - - - private func setupEvents(){ - header.billListEntryButton.addTarget(self, action: #selector(tapBillDetailEntry), for: .touchUpInside) - } - - // MARK: - Action - - @objc private func tapBillDetailEntry(){ - let v = BillDetailListController() - navigationController?.pushViewController(v, animated: true) - } - - // MARK: - TableView - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return bills.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "BillListCell", for: indexPath) as! BillListCell - let data = bills[indexPath.row] - cell.config(data) - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - print("didSelectRowAt \(indexPath.row)") - } -} - -extension CoinsIncomeTableController:JXPagingViewListViewDelegate { - func listView() -> UIView { - return view - } - - func listScrollView() -> UIScrollView { - return tableView - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - listViewDidScrollCallback = callback - } -} diff --git a/crush/Crush/Src/Modules/Wallet/CoinsRechargeGridController.swift b/crush/Crush/Src/Modules/Wallet/CoinsRechargeGridController.swift deleted file mode 100644 index 8b68130..0000000 --- a/crush/Crush/Src/Modules/Wallet/CoinsRechargeGridController.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// CoinsRechargeGridController.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -import UIKit -import JXPagingView -import Combine -class CoinsRechargeGridController: CLViewController { - - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - setupViews() - setupDatas() - setupEvents() - } - - private func setupViews() { - navigationView.isHidden = true - } - - private func setupDatas(){ - loadBalance() - loadCoinsTiers() - } - - private func loadBalance(){ - WalletCore.shared.refreshWallet { - - } - } - - private func loadCoinsTiers(){ - CLPurchase.shared.loadCoinTiersProducts {[weak self] reponse in - //self?.container.config(reponse?.productList ?? []) - self?.handleValidProductsInEpalServer(products: reponse?.productList) - } - } - - - - private func setupEvents(){ - container.header.billListEntryButton.addTarget(self, action: #selector(billListEntryButtonAction), for: .touchUpInside) - -// container.$selectIndex.sink { index in -// -// }.store(in: &cancellables) - - } - - // MARK: - helper - - private func handleValidProductsInEpalServer(products: [IAPProducts]?) { - guard let items = products else { - return - } - - container.config(items) - - Hud.showIndicator() - IAPCore.shared.requestProducts(items) { [weak self] iapIds in - Hud.hideIndicator() - var validProducts: [IAPProducts] = [] - for id in iapIds { - for per in items { - if id == per.productId { - validProducts.append(per) - continue - } - } - } - self?.container.config(validProducts) - } - } - - // MARK: - Action - @objc func billListEntryButtonAction() { - let vc = BillDetailListController() - navigationController?.pushViewController(vc, animated: true) - } - -} - -extension CoinsRechargeGridController:JXPagingViewListViewDelegate { - func listView() -> UIView { - return view - } - - func listScrollView() -> UIScrollView { - return container.cv - } - - func listViewDidScrollCallback(callback: @escaping (UIScrollView) -> Void) { - container.listViewDidScrollCallback = callback - } -} diff --git a/crush/Crush/Src/Modules/Wallet/Model/WalletInfo.swift b/crush/Crush/Src/Modules/Wallet/Model/WalletInfo.swift deleted file mode 100644 index 4d5e992..0000000 --- a/crush/Crush/Src/Modules/Wallet/Model/WalletInfo.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// WalletInfo.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -class WalletInfo: Codable{ - var withdrawable: Int? - var awaitingIncome: Int? - var income: Int? - var requestWithdraw: Int? - var balance: Int? - var wdFeeRate: Double? - - /* - { - "withdrawable" : 0, - "awaitingIncome" : 2, - "income" : 2, - "requestWithdraw" : 0, - "balance" : 9999999999, - "wdFeeRate" : 0.20000000000000001 - } - */ - - func displayBalance() -> String{ - let balanceCoin = Coin(cents: balance ?? 0) - return balanceCoin.thousandthsFormatted - } - -} diff --git a/crush/Crush/Src/Modules/Wallet/Sheet/CoinsRechargeSheet.swift b/crush/Crush/Src/Modules/Wallet/Sheet/CoinsRechargeSheet.swift deleted file mode 100644 index f570d9c..0000000 --- a/crush/Crush/Src/Modules/Wallet/Sheet/CoinsRechargeSheet.swift +++ /dev/null @@ -1,272 +0,0 @@ -// -// CoinsRechargeSheet.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -import UIKit -import Combine - -class CoinsRechargeSheet: EGPopBaseView { - var titleLabel: UILabel! - var descLabel: CLLabel! - var closeButton: EPIconTertiaryButton! - - var bottomView: UIView! - var balanceLabel: CLIconLabel! - var rechargeButton: StyleButton! - - var layout = UICollectionViewFlowLayout() - var cv: UICollectionView! - - // MARK: - 数据源和状态管理 - private var cancellables = Set() - @Published private var datas = [IAPProducts]() - @Published private var selectIndex = 0 - @Published private var selectProduct: IAPProducts? - private let viewModel = CoinsRechargeViewModel() - - init() { - super.init(direction: .bottom) - - contentView.backgroundColor = .c.csbn - contentLength = 549 + UIWindow.safeAreaBottom - - setupViews() - setupDatas() - setupEvents() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - contentView.backgroundColor = .c.csbn - - closeButton = { - let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) - v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(20) - make.size.equalTo(v.bgImageSize()) - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.ttm - v.textColor = .text - v.textAlignment = .center - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(32) - } - return v - }() - - descLabel = { - let v = CLLabel() - v.font = .t.tbs - v.numberOfLines = 2 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - // make.top.equalTo(titleLabel.snp.bottom).offset(24) - // make.top.equalToSuperview().offset(80) - make.centerY.equalTo(contentView.snp.top).offset(80 + 10) - } - return v - }() - - bottomView = { - let v = UIView() - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) - make.height.equalTo(80) - } - return v - }() - - balanceLabel = { - let v = CLIconLabel() - v.spacing = 8 - v.iconSize = CGSize(width: 24, height: 24) - v.iconImageView.image = UIImage.icon32Diamond - v.contentLabel.textColor = .text - v.contentLabel.font = .t.tndm - v.contentLabel.text = "0" - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.centerY.equalToSuperview() - } - return v - }() - - rechargeButton = { - let v = StyleButton() - v.primary(size: .large) - v.addTarget(self, action: #selector(rechargeButtonAction), for: .touchUpInside) - bottomView.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-24) - //make.leading.equalToSuperview().offset(<#T##amount: any ConstraintOffsetTarget##any ConstraintOffsetTarget#>) - make.leading.greaterThanOrEqualTo(balanceLabel.snp.trailing).offset(16) - } - return v - }() - - do { - let width = floor((UIScreen.width - 24 * 2 - 16) * 0.5) - let height = 96.0 - - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(CoinsRechargeGridCell.self, forCellWithReuseIdentifier: "CoinsRechargeGridCell") - - - contentView.addSubview(cv) - cv.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview().offset(136) - //make.bottom.equalToSuperview() - make.bottom.equalTo(bottomView.snp.top) - } - } - - titleLabel.text = "Crush Coin insufficient" - descLabel.text = "The Crush coin is insufficient and cannot continue, please recharge" - rechargeButton.setTitle("Recharge", for: .normal) - } - - // MARK: - 事件设置 - private func setupEvents() { - // 监听选择索引变化 - $selectIndex.sink { [weak self] index in - guard let self = self else { return } - if index < self.datas.count { - self.selectProduct = self.datas[index] - } - }.store(in: &cancellables) - - $datas.sink {[weak self] products in - self?.selectIndex = self?.selectIndex ?? 0 - }.store(in: &cancellables) - - // 监听钱包余额变化 - WalletCore.shared.$balance.sink { [weak self] wallet in - self?.balanceLabel.contentLabel.text = wallet.displayBalance() -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { -// self?.dismiss() -// } - }.store(in: &cancellables) - - NotificationCenter.default.addObserver(self, selector: #selector(notifyChargeDone), name: AppNotificationName.chargeDonePushTradeId.notificationName, object: nil) - } - - @objc private func notifyChargeDone(){ - dismiss() - } - - // MARK: - 数据加载 - private func setupDatas() { - loadBalance() - loadCoinsTiers() - } - - private func loadBalance() { - WalletCore.shared.refreshWallet() - } - - private func loadCoinsTiers() { - CLPurchase.shared.loadCoinTiersProducts { [weak self] response in - self?.handleValidProductsInEpalServer(products: response?.productList) - } - } - - // MARK: - 处理有效产品 - private func handleValidProductsInEpalServer(products: [IAPProducts]?) { - guard let items = products else { - return - } - - datas = items - cv.reloadData() - - // 显示加载指示器 - Hud.showIndicator() - IAPCore.shared.requestProducts(items) { [weak self] iapIds in - Hud.hideIndicator() - var validProducts: [IAPProducts] = [] - for id in iapIds { - for per in items { - if id == per.productId { - validProducts.append(per) - continue - } - } - } - self?.datas = validProducts - self?.cv.reloadData() - } - } - - // MARK: - 充值按钮点击 - @objc private func rechargeButtonAction() { - guard let selectedProduct = selectProduct else { - Hud.toast(str: "Please select a recharge option") - return - } - - // 开始购买流程 - viewModel.purchase(product: selectedProduct) { [weak self] success, tradeNo in - if success, let tradeId = tradeNo { - selectedProduct.tradeId = tradeId - IAPCore.shared.addPayProductId(productId: selectedProduct.productId ?? "", tradeId: tradeId) - } else { - Hud.toast(str: "Purchase failed, please try again") - } - } - } -} - -extension CoinsRechargeSheet: UICollectionViewDelegate, UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CoinsRechargeGridCell", for: indexPath) as! CoinsRechargeGridCell - let data = datas[indexPath.item] - cell.block.backgroundColor = .c.csnn - cell.config(data) - cell.setupSelected(selected: indexPath.item == selectIndex) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - selectIndex = indexPath.item - collectionView.reloadData() - } -} diff --git a/crush/Crush/Src/Modules/Wallet/View/BillListCell.swift b/crush/Crush/Src/Modules/Wallet/View/BillListCell.swift deleted file mode 100644 index 8aac1f2..0000000 --- a/crush/Crush/Src/Modules/Wallet/View/BillListCell.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// BillListCell.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -class BillListCell : UITableViewCell{ - - var block: UIView! - var icon: UIImageView! - var coinAndLabel: CLIconLabel! - var titleLabel: CLLabel! - var descLabel: CLLabel! - - var bill: BillListInfo? - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - selectionStyle = .none - backgroundColor = .clear - - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews(){ - block = { - let v = UIView() - v.backgroundColor = .c.csbn - v.cornerRadius = 12 - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview() - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - icon = { - let v = UIImageView() - block.addSubview(v) - v.snp.makeConstraints { make in - make.size.equalTo(CGSize(width: 48, height: 48)) - make.leading.equalToSuperview().offset(16) - make.top.equalToSuperview().offset(16) - make.bottom.equalToSuperview().offset(-16) - } - return v - }() - - coinAndLabel = { - let v = CLIconLabel() - v.iconSize = CGSize(width: 16, height: 16) - v.iconImageView.image = UIImage.icon32Diamond - v.contentLabel.font = .t.tnmm - v.contentLabel.textColor = .white - v.contentLabel.text = "0" - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.trailing.equalToSuperview().offset(-16) - } - return v - }() - - titleLabel = { - let v = CLLabel() - v.font = .t.tts - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalTo(icon) - make.leading.equalTo(icon.snp.trailing).offset(12) - make.trailing.lessThanOrEqualTo(coinAndLabel.snp.leading).offset(-12) - } - return v - }() - - descLabel = { - let v = CLLabel() - v.font = .t.tbs - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalTo(titleLabel) - make.bottom.equalTo(icon) - make.trailing.lessThanOrEqualTo(coinAndLabel.snp.leading).offset(-12) - } - v.text = "-" - return v - }() - -// testData() - } - - private func testData(){ - icon.backgroundColor = .random - titleLabel.text = "Gift" - descLabel.text = "Sep 9, 2025 11:11:11" - } - - func config(_ bill: BillListInfo?){ - self.bill = bill - guard let data = bill else {return} - - // -- Icon -// if let img = data.headImg{ -// icon.loadImage(img) -// } - icon.loadImage(data.headImg, bgColor: .c.csnn) - titleLabel.text = data.item - - // Desc - if let timestamp = data.time{ -// let date = Date.dateFromMilliseconds(Int64(timestamp)) - let display = Date.timerStyle(style: .WDMYHMS, millisecond: timestamp) - descLabel.text = display - }else{ - descLabel.text = "-" - } - - - let amount = data.amount ?? 0 - let coin = Coin(cents: amount) - let inOrOut = data.inOrOut ?? .in - coinAndLabel.contentLabel.text = "\(inOrOut == .in ? "+":"-")\(coin.formatted)" - - // Other - if let bizType = data.bizType{ - if bizType == "GIFT"{ - if let obj = data.getExtendObj(), let giftId = obj.giftId{ - AppDictManager.shared.getGiftModelBy(giftId: giftId) {[weak self] giftModel in - self?.icon.loadImage(giftModel?.icon) - } - } - } - } - - - } -} diff --git a/crush/Crush/Src/Modules/Wallet/View/CoinsIncomeViews.swift b/crush/Crush/Src/Modules/Wallet/View/CoinsIncomeViews.swift deleted file mode 100644 index 8a88de3..0000000 --- a/crush/Crush/Src/Modules/Wallet/View/CoinsIncomeViews.swift +++ /dev/null @@ -1,258 +0,0 @@ -// -// BillListViews.swift -// Crush -// -// Created by Leon on 2025/9/16. -// -import Combine -class CoinsIncomeHeadView: UIView{ - var block: UIView! - var bottomBg: UIImageView! - - var stackV:UIStackView! - var titleLabel1: CLLabel! - var coinAndLabel1 : CLIconLabel! - - var line: CLLine! - var titleLabel2: CLLabel! - var queryButton: EPIconTertiaryDarkButton! - var coinAndLabel2 : CLIconLabel! - - var billListEntryLabel: CLLabel! - var billListEntryButton: UIButton! - - var tableTitleLabel: CLLabel! - - static let heightOfHeader = 372.0 - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.backgroundColor = .clear - v.cornerRadius = 16 - v.clipsToBounds = true - v.layer.borderWidth = 1 - v.layer.borderColor = UIColor.c.con.cgColor - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.height.equalTo(292) - } - return v - }() - - bottomBg = { - let v = UIImageView() - v.image = UIImage(named: "walletBg") - let size = v.image?.size ?? .zero - v.contentMode = .scaleAspectFill - v.clipsToBounds = true - block.addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview() - make.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(size.height / size.width) - } - return v - }() - - stackV = { - let v = UIStackView() - v.axis = .vertical - v.spacing = 24 - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalToSuperview().offset(24) - } - return v - }() - - do{ - let view = UIView() - stackV.addArrangedSubview(view) - - titleLabel1 = { - let v = CLLabel() - v.font = .t.tts - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - titleLabel1.text = "Income" - - coinAndLabel1 = { - let v = CLIconLabel() - v.spacing = 8 - v.iconSize = CGSize(width: 24, height: 24) - v.iconImageView.image = UIImage.icon32Diamond - v.contentLabel.font = .t.tndl - v.contentLabel.textColor = .white - v.contentLabel.text = "0" - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.lessThanOrEqualToSuperview() - make.top.equalTo(titleLabel1.snp.bottom).offset(4) - make.bottom.equalToSuperview() - } - return v - }() - } - - line = { - let v = CLLine() - stackV.addArrangedSubview(v) - v.snp.makeConstraints { make in - make.height.equalTo(1) - } - return v - }() - - do{ - let view = UIView() - stackV.addArrangedSubview(view) - - titleLabel2 = { - let v = CLLabel() - v.font = .t.tts - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.top.equalToSuperview() - } - return v - }() - titleLabel2.text = "Pending" - - queryButton = { - let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .xs, iconCode: .question) - v.addTarget(self, action: #selector(tapQueryButton), for: .touchUpInside) - view.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(titleLabel2) - make.leading.equalTo(titleLabel2.snp.trailing).offset(4) - make.size.equalTo(v.bgImageSize()) - } - return v - }() - - coinAndLabel2 = { - let v = CLIconLabel() - v.spacing = 8 - v.iconSize = CGSize(width: 24, height: 24) - v.iconImageView.image = UIImage.icon32Diamond - v.contentLabel.font = .t.tndl - v.contentLabel.textColor = .white - v.contentLabel.text = "0" - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview() - make.trailing.lessThanOrEqualToSuperview() - make.top.equalTo(titleLabel2.snp.bottom).offset(4) - make.bottom.equalToSuperview() - } - return v - }() - } - - billListEntryLabel = { - let v = CLLabel() - v.textColor = .c.cpvn - v.font = .t.tls - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-24) - } - return v - }() - - billListEntryButton = { - let v = UIButton() - // v.addTarget(self, action: #selector(billListEntryButtonAction), for: .touchUpInside) - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(billListEntryLabel) - make.leading.trailing.equalTo(billListEntryLabel) - make.height.equalTo(30) - } - return v - }() - - tableTitleLabel = { - let v = CLLabel() - v.font = .t.ttm - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalTo(block.snp.bottom).offset(24) - } - return v - }() - billListEntryLabel.text = "Transaction Details>" - tableTitleLabel.text = "Creation Income" - - - } - - private func setupEvent(){ - WalletCore.shared.$balance.sink {[weak self] _ in - self?.refreshBalanceShow() - }.store(in: &cancellables) - } - - // MARK: - Helper - - private func refreshBalanceShow(){ - let balance = WalletCore.shared.balance - let income = balance.income ?? 0 - let pending = balance.awaitingIncome ?? 0 - let incomeCoin = Coin(cents: income) - let pendingCoin = Coin(cents: pending) - - coinAndLabel1.contentLabel.text = incomeCoin.thousandthsFormatted - coinAndLabel2.contentLabel.text = pendingCoin.thousandthsFormatted - } - - // MARK: - ACtion - @objc func tapQueryButton(){ - let content = "获得的收益30日后可提现." - let alert = Alert(title: "Tips", text: content) - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) { - } - alert.addAction(action1) - alert.show() - } - -// @objc func billListEntryButtonAction() { -// print("billListEntryButtonAction") -// } - -// func config(_ model: BillListResponse?){ -// let income = model?.incomeTotal ?? 0 -// let pending = model?.outcomeTotal ?? 0 -// let incomeCoin = Coin(cents: income) -// let pendingCoin = Coin(cents: pending) -// -// coinAndLabel1.contentLabel.text = incomeCoin.formatted -// coinAndLabel2.contentLabel.text = pendingCoin.formatted -// } -} diff --git a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeBottomView.swift b/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeBottomView.swift deleted file mode 100644 index 6acc499..0000000 --- a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeBottomView.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// CoinsRechargeBottomView.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -import ActiveLabel - - -class CoinsRechargeBottomView: UIView { - var operateButton: StyleButton! - var activeLabel: ActiveLabel! - var checkButton: EPRadioButton! - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .c.cbd - activeLabel = { - let v = ActiveLabel() - v.textColor = .text - v.font = .t.tbs - v.numberOfLines = 0 - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(48) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom - 16) - } - return v - }() - - checkButton = { - let button = EPRadioButton() - //button.setupStyle(.rectangle) - button.setupRadioStyle(.checkEmpty) - button.addTarget(self, action: #selector(tapCheckButton), for: .touchUpInside) - addSubview(button) - button.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.top.equalTo(activeLabel).offset(2) - make.size.equalTo(CGSize(width: 16, height: 16)) - } - button.isSelected = true - return button - }() - - operateButton = { - let v = StyleButton() - v.primary(size: .large) - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(CGFloat.lrs) - make.trailing.equalToSuperview().offset(-CGFloat.lrs) - make.top.equalToSuperview().offset(16) - make.bottom.equalTo(activeLabel.snp.top).offset(-16) - } - return v - }() - - operateButton.setTitle("Recharge", for: .normal) - - setupAgreenment() - } - - // MARK: - Public - - // MARK: - Helper - - func setupAgreenment() { - let userAgreenment = "CrushLevel Top-Up Agreement" - let full = "I have read and agree to the \(userAgreenment)" - let activeType1 = ActiveType.custom(pattern: userAgreenment) - - activeLabel.text = full - activeLabel.enabledTypes = [activeType1] -// activeLabel.customColor[activeType1] = .red - activeLabel.handleCustomTap(for: activeType1) { str in - dlog("tap\(str)") - AppRouter.goRechargeH5() - } - - activeLabel.customize { label in - label.configureLinkAttribute = { type, attributes, _ in - var attr = attributes - if type == activeType1 { - attr[NSAttributedString.Key.foregroundColor] = UIColor.c.cpvn - attr[NSAttributedString.Key.font] = UIFont.t.tbsm - } - return attr - } - } - } - - // MARK: - Action - - @objc func tapCheckButton() { - checkButton.isSelected = !checkButton.isSelected - - operateButton.isEnabled = checkButton.isSelected - } -} diff --git a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeGridCell.swift b/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeGridCell.swift deleted file mode 100644 index 4e01195..0000000 --- a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeGridCell.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// CoinsRechargeGridCell.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -import UIKit - -class CoinsRechargeGridCell: UICollectionViewCell{ - var block: UIView! - - var stackV: UIStackView! - var iconAndLabel: CLIconLabel! - var priceLabel : CLLabel! - - var data : IAPProducts? - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.cornerRadius = 16 - v.backgroundColor = .c.csbn - contentView.addSubview(v) - v.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - return v - }() - - stackV = { - let v = UIStackView() - v.spacing = 8 - v.axis = .vertical - v.alignment = .center - block.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.centerY.equalToSuperview() - } - return v - }() - - iconAndLabel = { - let v = CLIconLabel() - v.spacing = 8 - v.iconSize = CGSize(width: 24, height: 24) - v.iconImageView.image = UIImage.icon32Diamond - v.contentLabel.textColor = .text - v.contentLabel.font = .t.tndm - stackV.addArrangedSubview(v) - return v - }() - - priceLabel = { - let v = CLLabel() - v.textColor = .c.ctsn - v.font = .t.tnms - stackV.addArrangedSubview(v) - return v - }() - - testData() - } - - private func testData(){ - let coin = Coin(cents: 530) - iconAndLabel.contentLabel.text = coin.formatted - - priceLabel.text = "$4.99" - } - - // MARK: - Public - - func config(_ data: IAPProducts?){ - self.data = data - guard let tier = data, let chargeAmount = tier.chargeAmount, let payAmount = tier.payAmount else{ - return - } - - let coin = Coin(cents: chargeAmount) - iconAndLabel.contentLabel.text = coin.formatted - - if let currencySymbol = tier.currencySymbol{ - let coin = Coin(usd: payAmount) - priceLabel.text = "\(currencySymbol)\(coin.formatted)" -// priceLabel.text = "\(currencySymbol)\(payAmount)" - }else{ - let usd = Coin(usd: payAmount*0.01) - priceLabel.text = "$\(usd.formatted)" - } - - } - - func setupSelected(selected: Bool){ - block.layer.borderWidth = selected ? 2 : 0 - block.layer.borderColor = selected ? UIColor.c.cpn.cgColor : UIColor.clear.cgColor - } - - -} diff --git a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeGridView.swift b/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeGridView.swift deleted file mode 100644 index 807844d..0000000 --- a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeGridView.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// CoinsRechargeGridView.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -import UIKit -import Combine -class CoinsRechargeGridView: UIView{ - var layout = UICollectionViewFlowLayout() - var cv : UICollectionView! - var header: CoinsRechargeHeader! - - var headerHeight: CGFloat = 248 - var listViewDidScrollCallback: ((UIScrollView) -> Void)? - - var datas = [IAPProducts]() - - @Published var selectIndex = 0 - @Published var selectProduct : IAPProducts? - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - let width = floor((UIScreen.width - 24 * 2 - 16) * 0.5) - let height = 96.0 - - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: width, height: height) - layout.minimumLineSpacing = 16 - layout.minimumInteritemSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 0, left: 24, bottom: UIWindow.safeAreaBottom + 136 + 16, right: 24) - - cv = UICollectionView(frame: .zero, collectionViewLayout: layout) - cv.backgroundColor = .clear - cv.showsHorizontalScrollIndicator = false - // cv.showsVerticalScrollIndicator = false - cv.delegate = self - cv.dataSource = self - cv.contentInsetAdjustmentBehavior = .never - cv.register(CoinsRechargeGridCell.self, forCellWithReuseIdentifier: "CoinsRechargeGridCell") - cv.register(UICollectionReusableView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: "UICollectionReusableView") - addSubview(cv) - cv.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - //make.bottom.equalTo(operateView.snp.top) - make.bottom.equalToSuperview() - } - - header = CoinsRechargeHeader() - - $selectIndex.sink {[weak self] index in - guard let self = self else{return} - if index < self.datas.count{ - self.selectProduct = self.datas[index] - } - }.store(in: &cancellables) - } - - func config(_ tiers: [IAPProducts]){ - datas = tiers - cv.reloadData() - selectIndex = selectIndex - } - - // MARK: - Helper - -} - -extension CoinsRechargeGridView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{ - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datas.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CoinsRechargeGridCell", for: indexPath) as! CoinsRechargeGridCell - let data = datas[indexPath.item] - cell.config(data) - cell.setupSelected(selected: indexPath.item == selectIndex) - return cell - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - selectIndex = indexPath.item - collectionView.reloadData() - } - - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader { - let head = collectionView.dequeueReusableSupplementaryView( - ofKind: kind, - withReuseIdentifier: "UICollectionReusableView", - for: indexPath - ) - if header.superview == nil || header.superview != head { - header.removeFromSuperview() - } - - head.addSubview(header) - header.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - return head - } - return UICollectionReusableView() - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - return CGSize(width: UIScreen.width, height: headerHeight) - } - - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - listViewDidScrollCallback?(scrollView) - } -} - - diff --git a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeHeader.swift b/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeHeader.swift deleted file mode 100644 index e624107..0000000 --- a/crush/Crush/Src/Modules/Wallet/View/CoinsRechargeHeader.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// CoinsRechargeHeader.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -import Combine - -class CoinsRechargeHeader: UIView { - var block: UIView! - var bottomBg: UIImageView! - var titleLabel: UILabel! - var coinAndLabel: CLIconLabel! - var billListEntryLabel: CLLabel! - var billListEntryButton: UIButton! - - - private var cancellables = Set() - - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - setupEvent() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - block = { - let v = UIView() - v.backgroundColor = .clear - v.cornerRadius = 16 - v.clipsToBounds = true - v.layer.borderWidth = 1 - v.layer.borderColor = UIColor.c.con.cgColor - addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(16) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.height.equalTo(168) - } - return v - - }() - - bottomBg = { - let v = UIImageView() - v.image = UIImage(named: "walletBg") - let size = v.image?.size ?? .zero - v.contentMode = .scaleAspectFill - v.clipsToBounds = true - block.addSubview(v) - v.snp.makeConstraints { make in - make.bottom.equalToSuperview() - make.leading.trailing.equalToSuperview() - make.height.equalTo(v.snp.width).multipliedBy(size.height / size.width) - } - return v - }() - - titleLabel = { - let v = UILabel() - v.font = .t.tts - v.textColor = .c.ctpn - block.addSubview(v) - v.snp.makeConstraints { make in - make.top.equalToSuperview().offset(24) - make.centerX.equalToSuperview() - } - - return v - }() - - coinAndLabel = { - let v = CLIconLabel() - v.spacing = 8 - v.iconSize = CGSize(width: 24, height: 24) - v.iconImageView.image = UIImage.icon32Diamond - v.contentLabel.font = .t.tndl - v.contentLabel.textColor = .white - v.contentLabel.text = "0" - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.top.equalTo(titleLabel.snp.bottom).offset(4) - } - return v - }() - - billListEntryLabel = { - let v = CLLabel() - v.textColor = .c.cpvn - v.font = .t.tls - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(-24) - } - return v - }() - - billListEntryButton = { - let v = UIButton() - - block.addSubview(v) - v.snp.makeConstraints { make in - make.centerY.equalTo(billListEntryLabel) - make.leading.trailing.equalTo(billListEntryLabel) - make.height.equalTo(30) - } - return v - }() - - let rechargeTitleLabel = { - let v = CLLabel() - v.font = .t.ttm - addSubview(v) - v.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.top.equalTo(block.snp.bottom).offset(24) - } - return v - }() - - titleLabel.text = "Balance" - billListEntryLabel.text = "Transaction Details>" - rechargeTitleLabel.text = "Recharge" - } - - private func setupEvent(){ - WalletCore.shared.$balance.sink {[weak self] wallet in - self?.coinAndLabel.contentLabel.text = wallet.displayBalance() - }.store(in: &cancellables) - } - - -} diff --git a/crush/Crush/Src/Modules/Wallet/ViewModel/CoinsRechargeViewModel.swift b/crush/Crush/Src/Modules/Wallet/ViewModel/CoinsRechargeViewModel.swift deleted file mode 100644 index e7c6634..0000000 --- a/crush/Crush/Src/Modules/Wallet/ViewModel/CoinsRechargeViewModel.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// CoinsRechargeViewModel.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -class CoinsRechargeViewModel{ - - - func purchase(product: IAPProducts, completion: ((Bool, String?)->Void)? = nil){ - guard let productId = product.productId else {return} - if let tradeId = product.tradeId, tradeId.count > 0 { - IAPCore.shared.addPayProductId(productId: productId, tradeId: tradeId) - return - } - - var params = [String:Any]() - params.updateValue(productId, forKey: "productId") - params.updateValue("1", forKey: "version") -// if let chargeAmount = product.chargeAmount{ -// params.updateValue(chargeAmount, forKey: "chargeAmount") -// } - - WalletProvider.request(.tradePrecharge(params: params), modelType: CoinRechargePreOrderResponse.self) { result in - switch result { - case .success(let model): - completion?(true, model?.tradeNo) - case .failure: - completion?(false, nil) - } - } - } -} diff --git a/crush/Crush/Src/Modules/Wallet/ViewModel/WalletCore.swift b/crush/Crush/Src/Modules/Wallet/ViewModel/WalletCore.swift deleted file mode 100644 index cf96d18..0000000 --- a/crush/Crush/Src/Modules/Wallet/ViewModel/WalletCore.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// WalletCore.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -class WalletCore { - static let shared = WalletCore() - - @Published private(set) var balance: WalletInfo = WalletInfo() - - init() { - NotificationCenter.default.addObserver(self, selector: #selector(login), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - } - - // MARK: - Public - - func refreshWallet(postNoti: Bool = true, block: (() -> Void)? = nil) { - guard UserCore.shared.isLogin() else { return } - - WalletProvider.request(.myWallet, modelType: WalletInfo.self) { [weak self] result in - switch result { - case let .success(model): - if let balance = model { - self?.balance = balance - } - if postNoti { - NotificationCenter.post(name: .walletInfoUpdated, object: nil, userInfo: nil) - } - case .failure: - break - } - block?() - } - } - - // MARK: Public helper - - func displayBalance() -> String { - return balance.displayBalance() - } - - // MARK: - Noti - - @objc private func login() { - refreshWallet(postNoti: true, block: nil) - } -} diff --git a/crush/Crush/Src/Modules/Wallet/WalletMainPagerController.swift b/crush/Crush/Src/Modules/Wallet/WalletMainPagerController.swift deleted file mode 100644 index b2daaff..0000000 --- a/crush/Crush/Src/Modules/Wallet/WalletMainPagerController.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// WalletMainPagerController.swift -// Crush -// -// Created by Leon on 2025/9/15. -// - -import UIKit -import JXPagingView -import JXSegmentedView -import Combine - -class WalletMainPagerController: CLBaseViewController { - var titleView: TitleView! - - private let segmentedViewHeight = 40 - private lazy var segmentedView = JXSegmentedView(frame: CGRect(x: 0, y: 0, width: UIScreen.width, height: CGFloat(segmentedViewHeight))) - private lazy var pagingView = JXPagingListRefreshView(delegate: self) - private var controllers = [JXPagingViewListViewDelegate]() - private let dataSource = JXSegmentedTagStyleDataSource() - - var headerViewHeight = 0// 60 - - lazy var coinsRechargeVc = CoinsRechargeGridController() - lazy var incomeListVc = CoinsIncomeTableController() - - var operateView: CoinsRechargeBottomView! - - var viewModel = CoinsRechargeViewModel() - - private var cancellables = Set() - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - - setupViews() - setupDatas() - setupEvents() - - } - - private func setupViews() { - let title = "Wallet" - navigationView.alpha0Title = title - titleView = { - let v = TitleView() - v.title = title - return v - }() - titleView.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: titleView.preCalculateHeight()) - headerViewHeight = Int(titleView.preCalculateHeight()) - - pagingView.mainTableView.backgroundColor = UIColor.clear - pagingView.mainTableView.contentInsetAdjustmentBehavior = .never - pagingView.listContainerView.listCellBackgroundColor = .clear - if #available(iOS 15.0, *) { - pagingView.mainTableView.sectionHeaderTopPadding = 0 - } else { - // Fallback on earlier versions - } - pagingView.mainTableView.gestureDelegate = self - view.addSubview(pagingView) - pagingView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - //make.top.equalToSuperview() - make.top.equalTo(navigationView.snp.bottom) - } - - //dataSource.clNormalStyle() - - let titles = ["Recharge", "Income"] - dataSource.titles = titles - - segmentedView.listContainer = pagingView.listContainerView - segmentedView.dataSource = dataSource - segmentedView.delegate = self - segmentedView.contentEdgeInsetLeft = 24 - segmentedView.contentEdgeInsetRight = 24 - // segmentedView.clNormalStyle() - - operateView = { - let v = CoinsRechargeBottomView() - view.addSubview(v) - v.snp.makeConstraints { make in - make.leading.trailing.bottom.equalToSuperview() - } - return v - }() - } - - private func setupDatas(){ - - } - - private func setupEvents(){ - operateView.operateButton.addTarget(self, action: #selector(tapRechargeButton), for: .touchUpInside) - -// coinsRechargeVc.container.selectIndex - coinsRechargeVc.container.$selectProduct.sink {[weak self] product in - // refresh button title⚠️ - } - } - - // MARK: - Action - @objc private func tapRechargeButton(){ - - guard let selectProduct = coinsRechargeVc.container.selectProduct, let productId = selectProduct.productId else{ - return - } - - Hud.showIndicator() - viewModel.purchase(product: selectProduct) { result, tradeNo in - Hud.hideIndicator() - guard let no = tradeNo else{ - return - } - - selectProduct.tradeId = tradeNo - IAPCore.shared.addPayProductId(productId: productId, tradeId: no) - } - } -} - -extension WalletMainPagerController: JXSegmentedViewDelegate, JXPagingViewDelegate { - func tableHeaderViewHeight(in _: JXPagingView) -> Int { - return Int(headerViewHeight) - } - - func tableHeaderView(in _: JXPagingView) -> UIView { - return titleView - } - - func heightForPinSectionHeader(in _: JXPagingView) -> Int { - return segmentedViewHeight - } - - func viewForPinSectionHeader(in _: JXPagingView) -> UIView { - return segmentedView - } - - func numberOfLists(in _: JXPagingView) -> Int { - return dataSource.titles.count - } - - func pagingView(_: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate { - if index == 0 { - return coinsRechargeVc - } else{ - return incomeListVc - } - } - - func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { - operateView.isHidden = index != 0 - } - - func mainTableViewDidScroll(_ scrollView: UIScrollView) { - NaviAlphaHandle.changeNaviViewsAlpha(scrollView: scrollView, alphaViews: [navigationView.titleLabel, navigationView.bgView], oppositeViews: []) - } -} - -extension WalletMainPagerController: JXPagingMainTableViewGestureDelegate { - func mainTableViewGestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - // 如果是 UICollectionView 的手势,先判断滚动方向 - if let panGesture = otherGestureRecognizer as? UIPanGestureRecognizer, - let otherView = otherGestureRecognizer.view, - otherView is UICollectionView { - let velocity = panGesture.velocity(in: otherView) - // 横向滚动时禁止 - if abs(velocity.x) > abs(velocity.y) { - return false - } - } - return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self) - } -} diff --git a/crush/Crush/Src/Resource/Font/D-DIN-Bold.ttf b/crush/Crush/Src/Resource/Font/D-DIN-Bold.ttf deleted file mode 100644 index 5fcabb3..0000000 Binary files a/crush/Crush/Src/Resource/Font/D-DIN-Bold.ttf and /dev/null differ diff --git a/crush/Crush/Src/Resource/Font/OleoScriptSwashCaps-Regular.ttf b/crush/Crush/Src/Resource/Font/OleoScriptSwashCaps-Regular.ttf deleted file mode 100644 index fa31c45..0000000 Binary files a/crush/Crush/Src/Resource/Font/OleoScriptSwashCaps-Regular.ttf and /dev/null differ diff --git a/crush/Crush/Src/Resource/Font/Poppins-Bold.ttf b/crush/Crush/Src/Resource/Font/Poppins-Bold.ttf deleted file mode 100644 index b94d47f..0000000 Binary files a/crush/Crush/Src/Resource/Font/Poppins-Bold.ttf and /dev/null differ diff --git a/crush/Crush/Src/Resource/Font/Poppins-Italic.ttf b/crush/Crush/Src/Resource/Font/Poppins-Italic.ttf deleted file mode 100644 index 4620399..0000000 Binary files a/crush/Crush/Src/Resource/Font/Poppins-Italic.ttf and /dev/null differ diff --git a/crush/Crush/Src/Resource/Font/Poppins-Medium.ttf b/crush/Crush/Src/Resource/Font/Poppins-Medium.ttf deleted file mode 100644 index 6bcdcc2..0000000 Binary files a/crush/Crush/Src/Resource/Font/Poppins-Medium.ttf and /dev/null differ diff --git a/crush/Crush/Src/Resource/Font/Poppins-Regular.ttf b/crush/Crush/Src/Resource/Font/Poppins-Regular.ttf deleted file mode 100644 index 9f0c71b..0000000 Binary files a/crush/Crush/Src/Resource/Font/Poppins-Regular.ttf and /dev/null differ diff --git a/crush/Crush/Src/Resource/Font/Poppins-SemiBold.ttf b/crush/Crush/Src/Resource/Font/Poppins-SemiBold.ttf deleted file mode 100644 index 74c726e..0000000 Binary files a/crush/Crush/Src/Resource/Font/Poppins-SemiBold.ttf and /dev/null differ diff --git a/crush/Crush/Src/Resource/LottieResources/IM/im_chat_msg_dislike.json b/crush/Crush/Src/Resource/LottieResources/IM/im_chat_msg_dislike.json deleted file mode 100644 index 67de5e8..0000000 --- a/crush/Crush/Src/Resource/LottieResources/IM/im_chat_msg_dislike.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":30,"ip":0,"op":40,"w":108,"h":108,"nm":"dislike","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"dislike","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19,"s":[-10]},{"t":24,"s":[0]}],"ix":10},"p":{"a":0,"k":[24,25.774,0],"ix":2,"l":2},"a":{"a":0,"k":[-6.736,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[201,201,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[226,226,100]},{"t":24,"s":[201,201,100]}],"ix":6,"l":2}},"ao":0,"sy":[{"bm":{"a":0,"k":1,"ix":1},"c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":7,"s":[1,1,1,1]},{"t":8,"s":[0.901960790157,0.235294118524,0.54509806633,1]}],"ix":2},"so":{"a":0,"k":100,"ix":3},"ty":7,"nm":"颜色叠加"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.09,-0.883],[0,0],[0,0],[-0.967,0],[0,0]],"o":[[0,0],[-0.906,0],[0,0],[0,0],[0,0.967],[0,0],[0,0]],"v":[[-4.486,-7],[-5.736,-7],[-7.477,-5.429],[-7.486,-5.25],[-7.486,0.75],[-5.736,2.5],[-4.486,2.5]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-1.059,0],[-0.527,0.581],[0,1.005],[0,0],[0,0],[0,0],[-0.549,0.664],[0.132,0.809],[0,0],[0,0],[0,0],[0,0],[0.481,0.389],[0,0],[0.667,0],[0,0],[0,0]],"o":[[0.707,0],[0.525,-0.578],[0,0],[0,0],[0,0],[0.858,0],[0.534,-0.646],[0,0],[0,0],[0,0],[0,0],[-0.131,-0.611],[0,0],[-0.503,-0.414],[0,0],[0,0],[0,1.367]],"v":[[-0.736,7],[1.207,6.154],[2.014,3.75],[2.014,3.508],[4.609,3.531],[4.616,3.532],[6.757,2.525],[7.354,0.317],[7.354,0.249],[7.329,0.152],[5.977,-4.824],[5.976,-4.823],[5.03,-6.36],[5.029,-6.359],[3.264,-7],[-2.986,-7],[-2.986,4.75]],"c":true},"ix":2},"nm":"路径 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960790157,0.235294118524,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[8.236,7.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/IM/im_chat_msg_like.json b/crush/Crush/Src/Resource/LottieResources/IM/im_chat_msg_like.json deleted file mode 100644 index 615008d..0000000 --- a/crush/Crush/Src/Resource/LottieResources/IM/im_chat_msg_like.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":30,"ip":0,"op":50,"w":108,"h":108,"nm":"like","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[100]},{"t":28,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[54,54,0],"ix":2,"l":2},"a":{"a":0,"k":[0,1,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":12,"s":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":16,"s":[100,100,100]},{"t":28,"s":[120,120,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":16,"s":[10,10]},{"t":28,"s":[1,1]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":50,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-2.118,-43.368],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"rp","c":{"a":0,"k":6,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":2,"tr":{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":61,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"变换"},"nm":"中继器 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"like 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[53.983,54.016,0],"ix":2,"l":2},"a":{"a":0,"k":[8.19,7.75,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[403,403,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1.063,1.063,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[300,300,100]},{"i":{"x":[0.667,0.667,0.667],"y":[0.866,0.866,2.065]},"o":{"x":[0.333,0.333,0.333],"y":[0.1,0.1,0]},"t":15,"s":[460,460,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[-0.467,-0.467,-1.065]},"t":19,"s":[380,380,100]},{"t":23,"s":[403,403,100]}],"ix":6,"l":2}},"ao":0,"sy":[{"bm":{"a":0,"k":1,"ix":1},"c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[1,1,1,1]},{"t":16,"s":[0.901960790157,0.235294118524,0.54509806633,1]}],"ix":2},"so":{"a":0,"k":100,"ix":3},"ty":7,"nm":"颜色叠加"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.09,0.883],[0,0],[0,0],[-0.967,0],[0,0]],"o":[[0,0],[-0.906,0],[0,0],[0,0],[0,-0.967],[0,0],[0,0]],"v":[[-4.486,7],[-5.736,7],[-7.477,5.429],[-7.486,5.25],[-7.486,-0.75],[-5.736,-2.5],[-4.486,-2.5]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-1.059,0],[-0.527,-0.581],[0,-1.005],[0,0],[0,0],[0,0],[-0.549,-0.665],[0.132,-0.809],[0,0],[0,0],[0,0],[0,0],[0.481,-0.389],[0,0],[0.667,0],[0,0],[0,0]],"o":[[0.707,0],[0.525,0.578],[0,0],[0,0],[0,0],[0.858,0],[0.534,0.646],[0,0],[0,0],[0,0],[0,0],[-0.131,0.611],[0,0],[-0.503,0.414],[0,0],[0,0],[0,-1.367]],"v":[[-0.736,-7],[1.207,-6.154],[2.014,-3.75],[2.014,-3.508],[4.609,-3.531],[4.616,-3.532],[6.757,-2.525],[7.354,-0.317],[7.354,-0.249],[7.329,-0.152],[5.977,4.824],[5.976,4.823],[5.03,6.36],[5.029,6.359],[3.264,7],[-2.986,7],[-2.986,-4.75]],"c":true},"ix":2},"nm":"路径 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960790157,0.235294118524,0.54509806633,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[8.236,7.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/Meet/meet_left_swipe_dislike.json b/crush/Crush/Src/Resource/LottieResources/Meet/meet_left_swipe_dislike.json deleted file mode 100644 index 453b77e..0000000 --- a/crush/Crush/Src/Resource/LottieResources/Meet/meet_left_swipe_dislike.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.9.6","fr":24,"ip":0,"op":48,"w":300,"h":300,"nm":"左滑不喜欢","ddd":0,"assets":[{"id":"image_0","w":144,"h":158,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACeCAYAAADdTIPIAAAACXBIWXMAAAABAAAAAQBPJcTWAAAAJHpUWHRDcmVhdG9yAAAImXNMyU9KVXBMK0ktUnBNS0tNLikGAEF6Bs5qehXFAAAgAElEQVR4nOy9e3hcx3Un+Dun7u1uoAmSgASBoCkDsmlNgo5jx4yT8diRIMfxeL/Er4yhrGf2yzuztjcTO5v4kV0nvLAzcV6bzWZkjx/J5PlNxsKMZSWO18mKEmzLb9PWI43EEm2TEgUQhESQBBro7nvrnP2jqu6tBkFLsh17dj8Wv2Y/0H1v3apf/c7vnDpVF7hSrpQr5Uq5Uq6UK+VKuVKulCvlSrlSrpQr5Uq5Uq6UK+VKuVKulCvlSrlSrpQr5Uq5Uq6UK+VKuVL+Oyr07a7AV3V9f7ObvgImvRHQZ5PKNBHvJ1e5k0Q4yYoPSqq3j9LQyW93fb8V5YvrK9N5Wnu5iMyq0rMVOq1QCOg8oCcJuCcXvf2m0YkPfrvr+m0D0Fd1ff9wv/F6qLxBiTxgqKwUgUDknwEQERhYtIn9qf+/Aulz6+uzSPSoqJ0VBQQKVcBCAQVAgKq6BlIARCcN8Z8UUvzpTaOTJ78ddf62AOjh7Y3ZhMwfA5gG1IPEtQpRAMwgeEpQAWCm399j0l/8dtT9n6J8cX19PxIcLdS+QaCwClhVuH/uNQCoAiANWKoGHOEkVH7xptGD33JG+pYDaKXXOSqCjCgMqgCOy4DGPzN2AItwT5+TV44SnfxWX8M3s3xxfX2/pnqXFXm2qKJQhUAhAEQVog4wgY129hgjsDOBQNlNoxPz38r6f0sB9Mj2Rgbw0fKsGgDhqsIlgCqwcHhPGASWe32S2Nw09P9REH1xe30aVu+yItNWFRYK60Fj4cxVABOAEkyhhGZkIvcAAMH8D141mX2rruGfGkDBWuOhzsVXMPNt4SOCQ8RO7cODAHEjDK6BAA8oqoAE0Ekw3zREdEo91ZP7btzW/70Vuvf8mWmb1O4UkWkLwKpAFLBwz6LqzZhjH/hnBaorI4C0agvjgWSAm2b3H/jot6Id+J/gmDTwrKCT589cx2z+TwVKu66ekt3DfVICxz92Vo4A12LhSQGCTpPIXdu6PQ0qwYMdh/q2FFWNz13W5TPnz0xrUrtTRadjU2WhJXgECiV4Bqr+pnCi2r2PWMs/OybDH9994aH9Hjr/pO3wDQPINxKF154FCB4cAGDqzZ8AdLr8DUqnAk44V1cX/gYix1NEuwprDL6fZq3dCcVUdYCyfuHltxxIRFS1jwJQxb3nz1zXSOp3iui0eBC4h6urQiGEUgMFxlFVZ9LEDzx4cGl4rgAkqtNdW3u9+23VGA6S39x2+IYBNDDiyXVqxbOBWfgnAuu4L7oS3ocGqUwaBr4Xna08J3t9FD4lxXQuctc2dBrzGe1A6bcUPKpKnlCVgrcA4N7u6jQltTtFZTowRmCY8qGAipbAV9VqTFRD1YFJXRs6xkJ0DAVIX79wuj0KoqqBNeqrbxIrfaMHqH5f2WXNsowz/3b5jf/ryyzzbW4AOrYJqieO9wQvK2gg91wJ60obDX6folr4Nj9p+/0XDg0NnXTnI4RnaARion8KbVBGaAbaBIrPXTh7XT2pHRPVaauKQqU0QyWYIjZxDFMBqNRBAVD+ObAw4NqECTDEMCAw9Kd/6D+8708BQI8e1RAqgbv2uO+/7rb4uhnIj7KSXoO62WH3kYNfobvVUS9Fb0lmRIivkGjwOwNufwQsdgw4zbXaXee73etofp6QZUTz8xROT0RKFMPum1Iouu6K/fyFfu7C2euGktoxVZ0u9Q0c4MV/VQfUcQC6pzEdbLuYTEoXP34umY1uRLvtLnZ+njCfkYIqqVGSnJYS5Mle+NcNIO9FudDE/Dxhfp5ofp7m5+cpQwbMtGlupk1MeLaz35ceI3ImLnkfutg14OD3UP5tkLUACsCcGkrTY+ff8pZpAMhQdUiWZeyBDgx2/NffFPCmPHgFALL5jDCf0ecunL1uOHXMU3ZsGd9RhLaJnYpL2ii0g2OPYMWqv0eDOAYokT4bc0AAkaunb7f5eQJBnQQNl/Dkm+LJ/oJUvQuO0FDz1J6ZoYWlJffBjKvs7FKLxttt/T/+9I8fBXT/YPzC1Z2oivEE+o3NFZcBssilH3h2v3FI3uHdARDoqYsbmy+c+P3fP6lHj2rZQCEiN3j1T4rGVZVK8xirOiJQlhEA3PeG104naf2YFfVxHueiF1CICixQAknEe11B12BQ/4gKFOTBoSVjiW9LgJwbH7Vbynz+xX/wr8fRHtc5AAtzAJZaCgDZ0aPIymuOlYg+KfP+hAAURikRKVRJfae7eRnXYHMzbVoAgKU1wsy4YqlFmFymh3789/phdMSKtorjREApgbRDD6HyxHaCK76IUitoRecKnNrIixdONBpfzbKMMgDIsioit0OtPNF2G5iTKiGkwPx8CZ40rR+zXvOEQKGIwlLleVmVEvjV/JcDk8bXEbSQb/MAvKhKpWPBREiIYIjxv70lq40cPKgAsNjOFJgDPJAyANnRowPGMwwyrxsft12ekAkLrrQzyQ7xmaozWVlGKMHTIkxeT1haoyOTy4SVdao4Ocaqax2Nmn7Q/qO8iOq9Rr92vylHY/Q6Fsr+aWpPwneudrvXZXDmDFnmZpTms9jhe3JsHPSo/5kzWfOEmTZ97g2vna6l9WOimK7c9AAMzx5RPTV65Ru88kxBA/B23TAIHN9Y1UclryiO4zgWl5dpEYtAa9Z9ecH9PWu3CfPzFPohgCfLsifcFk8EQOXBFK4Bsvl5yubnkQFoz7TJQRqYBYCVg3pk8nrqrqxTy/3m1G4HDXw00BgVci5bmTK871tY1ZsBRTWC48MpoKCpZsJ3rr7lDdNZu00ZgPn5eYIbfXHfPW7DeTZWiliH5jNqz8w45nnZLVMjtfoxVUyLyiVuemCTwDSD1x70jUaIDvyE8jFoeHzFI4scSZlTrYNPIxwBWhjnWcwCrTVCa40GdNH8vENplhFUkWWZPlE99LgAKtW6lvEWN4oBZJ555rCA2aUWbUxOEiaXqbuyTgDQfmyTRHHfpVXR6tj+bcw6Va9GmqCqxmAgrWShio0C1VefAApMNdPhYw+975bpDP4a5ucpyzIeRByA3YFEQfdAg+wgZ7LabWotLenn3vDa6brUj6lg2qpUwcBwmQqIxHQyiNuYh+I2iGmyQtAOAMbyAKXzcao3doBaK+vUGztAG5PL5Ic5gAU/7jNoluncwgIjyzQ6/OXaYaA8LoAognZoNwBAu02zSy3CArCwACwuL1N35Q5qraxTG0Bv7ABNXzVBuS0+PlgTvaRaA3xD8biL/x4DxAfSEIAUu8OenRADz5s5wfTe4f3HHnrDa68LwyCDNz+VCQzKJu4LVzVvehHMnxutAIAfee1PX7cvbRxTwXSYAI09LEEFjtiN361NSq8ysGwAmFbfc09cvg6DOzIX6Pd7HypGNqiDJgMn0F1ZJ0wuu6+0ZglLa4Q2iObmeGFpScuQx2W6ZrfyNQFUxnqi45EfcQDg7GqL8KJRxsEKOIfHDlBxboNO7u3QA2ce+dAgYCjW0/6o0ciLhmv5aWAfHWSoABaNH4g6KYBHUb4GYWpkeN+xU285Pz03M0MBSEpUUnkwU6EBw3vvSgDz86RZpnM338zZ3Bzd975bpvbv3X+HQKfL4GAwpxTAFIKBO9g3buDArCqhrqiArQNfd1payw50jslgaOOhR1c/dNWpGhcXOlScm6De2AGnSzHOmLzesVFrltBaozlvgnfUKYQ5LstEXxNAJft4c0CO8jE3B8w52YMjk8t0BEBrpoXDYweoGNmgbnGG+6jxhG3wz9z+2w9ZKwMstLPp/CkGTFAASGy2BjREaaJicxaxU3k8HRDbruNoag/V7/zdl714CjNzThNdyjRuOiK46QqiLCPMZzQ3M0N06xxjDrjvz2+ZGq41jlnR6QEXHGFiNAKvVkwZlypOFoBCZSOUbUTBRHnbEmkeRpy1ABgQFPLxt33kXQ+dbm6T7BulYm+HinMbhMc2y+s8MjlJs5jF3My4LmChkiaYL83AYNTp0vK1ABRptMpXym6do4WlFi0stWgWszgOoDt6kACgOLdBxfooTTRbdHVzL8nwCI3U95tHNtZ/8xJ3JxpJpVnyJ9QIHLGWKcUnwmRjlHRVvt4JqKqDymkAKEA01aT6nade9r4ptNuUhdYLFx3YqHxfeSatpSWdA/CrL75lqilDpbfl4jmI6hnYU8KVVReN+CWVIZFq7MfV8QN4oHOqfyFnqgqHEI6fPvE6KylLp0vLAOTiKAHA9FUTdNjrouPH34uNyeUy/BIcjGweyJy/WCH2MuXxRJKz894uZkAZKMQCcORFo9z1mmd2qsUn1t3fpDlKyyvrfPXQCD263aPRRpM+9drf+M3htP6anSdg77KGIGL4LNAxo4obhQqH4ONuRXe8jiclCeTiK9Cy4wA5dX574wef8XM/f9KFSOZQBkUROQyAi+i2Wop2Rp9/3+r0WG3oDqvObIVEsGqS1AlmDSYNg+xUOgIxk5YsGw8eVJrOgyxcW2gX8h8bcsHEfj9/90v/9E1vwXBNr96uK203NJ0sBCsA713X5GJTk7ERPXHujGJyVGdXDur4TFsXFpxLhJYLNiLLdrGzO/rvcn/QMNsyP+9QGbl9WGoRRkc5MM80mnxi/QLJxVEas1Oce/BYTXg/Uh5Rw3/x6Tt/W1T/PlamwY2vJoxLDoo0TZXeOWCuAtuUHRFppFgbBSABiPVJxWs0NdIYOXb/+26ZBoCFhYVSGGfInMiecSNzDgBm2oPgic4VJ4HFLDrAsKUwjl2C6PsxWHzXkU9xiF3rcmD5wWd84JCA+1/zx7/71n2aMrb6ZDVhHepSvrLOtrlNp6M+PvzYJh0BsAi42NBcBZ7Mn/vxYkKXBRDtCGCh1VLMtAlLazSLRRw5ArSX2s7bmprGaZzGMlawilWoB4/W+7S3PkRWO/yf7r9j48P/+PmfFJWH/QkQbH0QjIMmrMp3ifXLoFiOYkIYNG9h5JYPxHqo6lRRBRGmrm7sOXb/jbdMo7VG2a1zNAtwe2aOgEX2DcoLc8D9L75leqw+dEeY2xpkmADiQeYrmzGY613CFwPmyzdQaT61mi4hVODhkNZCBAMCVB7++IP3/NRFbGCPJrxPU17HOmSoSTo0QtLpEi52qdjboRPn7ic84zAu9OqM0oy13DwmStcCWZYJvka5BECR6nZeuw91w58AABYnr6fjK+uExzbpxLkNOrHepoPNUZpo7qfVTpdkqEkAsLfrwDO0x7BV5qPH/uyRhfbn/pVVPY0gGqOnsoHj+kQgKpPNEWsf9yiZALHIjgX5YOcG/RQAAGDqqn17jh1/3a3TWGrReDvThR3X/fkX3zK9pz50h4RMwrhuAaQKqJ+eqIKaEbMGFCBiXFWoSuVk+GNW8YQIPMG0e71jAnigDx974POves+nP3h6OB02F2sN2jMyyodqe43YDX50qEc6NEJo7idpjtI0plGc23AHXlmnIy8aZUwuU9ZG5ZG5LIaQ27QrE10CoCiczYhnqtttmsUiMHk9HTl+HIfHDnjzVQk06XRpbOwA61aPRtSw1Q5bGN7oFbSZFqRpjd5x958tv//+u+es6umyVUIjhzA/qlEqHmgDJipikYEYUABW+bdBxipZSKWMI8ViXIGpiZHmHcdf9zPTa61ZOrI+ykcmrydgEZ983c9Mj9Ui8EQgDCCqTNOA/B4YFKXHudNUla3g3pNHPZdjOTgfYc4LMN7jgurpv/3Hz978hx//m+XNTpdFuzys26zdnC50c9L6EI1uNUm2ezQ+PEL24jYVeyPPDMBxAEcAoDVLC1jAEy27mTD1tk8UKAU04G3l8eM47t9PXzVBJy90CJNAvpKwDo3Qua0zpPU+We2w1hqkaZ00rdNYT4z0xdTV8P+1+F+W/+L+j77ain2knIMJxEeDYAg+TGx+Yu8rTo2oWCA8AsBiLRXpKKk8u0pn6dTVI/vumP+ZP7ruOIDuyjp98nV/Pj3R3HuHaBznqVTUzjhPzHpADPzoGRW7KC4FVWX4AnioMlne82IQVOSRD//9p179l5/+8AoSS0NDzJrWSNSw1W1uwvCe88yim6yNJulml2R4hKTZIeCQO8WYc+2PHwdmQ6R6pr0b41zymdkVVvOu3jRPbvZ6pk1oAUeSl/MKrVNrZJS+dPoMnR/eQwcbezjfXGeMNyGmzo08JW3WOBXD0s9p04KTlNlYmCSxpMoslJjPLv/DJgkfe/bkdT/EzHsdZbsElaCPBjzZUP+K16Nrch/owLcJA92g1egOEV313eMd1hJwAPbvGx5+2Y8+/Tl//cPPfdH+yeboHQCmgomqEra8gx4Jfo2OH84YZbWWVa/e68Dr6rKqQA9FXqnxQDLO83jkg/cs/ptbv/C3y4BBCkIBQiMnKgBQLVWgBkoscukregD27gFtbQP9HmhT8TC+rIeHvhPn1h7FkeuH8KnN48BiEyDvic0u7phGGCyXIKqc73HtQHTzHKO1oLPIeGNymY4fPw4cfBphbJMOFXs4tw1e7XQJWz0aHTvAopu8RxO2MGwTNZrWSDtdrg8Z3lLmuvZZlVmThDSvmZ9+7g2HXvt9L/mzlJODIf5LoQFDz9MgnIIiCB0Ta4cBgJWaKgZSJGo9IHb6C0DpOp8Kpg2gyvsL4Al6KgArApBE5x3UZFqZZSD6vpb1BTwha3W9g+ksBFG7/IEvfuzH/9v9H32YbK6EmvRIZJhUqDBKJLJedO0eNIRqPU1ysp0htjRcU3O+ENrTUN7a0NR0pXTr//6TgoMj7qwz4+55qaXBnd8txWM3DeS/DJoPwbP2HOFGR3Gtg0+jw2MHaPrcBDntMwFgHE8bO8Ba79NILycLw9ovSPOCVLtch2FV5roSi6kZBbHmalLk/GdfWFx512f+5qdyKZajrBrEOURh1A+IYgy6z4FhStc/8rxKCRt1og50IKJAZBTHAabUPXwymFQ5y7Ee28E+cdLXzsWAJUBLDOsgoYZ+8HX2abpVohgBqnb5/Z+786cW7ju2khZqUmVWENeVWZRYlHkbwHBSo05eUKcDaK1Be9TwyDnDMtQrTxf064lzZxRHgCOTr3Yxv6UFX+2srOluM/S7mzAFzROwODtLuOYaYHaNTn5+iXD9VbSvtp+Lcxt0EsDIpCFrLA9rl7bAhI6ltJawgklHUta8IEWdE05Y04TVwiA1rIWymoRYYFT65t5Hv7J5odtf/P5Dz7iJiUcurc6OwGz0KJmGgumgyFQNfnfn5+ETKd/771Fgg0poI+ipWMsMCPbq99WLQaNa6SIdqH8JPhqcojBURZqD2VKR5b/43Ed+9sPtT60YEMACaEGmDlhbAEhAddI0BfJiGzVSre1tAN0CVBTg/TU9s7mB4dyAz4pyLcWhvXtxurmM1vgEku46XvBU1neNt8hlL85CFxcHpnrisuuHqkohaku3zrFL1XCzuBd6D3NxboKKvaOU20dZOl1aGxqh/TUxe+tDVGxZY3WbN9VwI3GiWfpqtDHEon1OczUKZjWGlNVov2CtG9K84Fd/1w1P+cUfeMV7aiaZjHXBQFVLfeH4Si53EdFvQ1+WIIu6tgRdCQyUJq1is8gUlsByR4gDh9XKiUHz5QCmEaAjxow+303iVatTCNYWK3/22Y/83EeWPr1CSIRIJYdK3bASqeTbfUuNVNiS7ZEKW7IMESIRJitJTnajv628rylscjnXYzsBIDVdSUab2jzVEaCF9kzbAnOAj8hnAI66lOBLyHTXQCL5UUyqwFJLF2faenzlAf/jwwCA0w+fBlYBHRqhMR/3Od99lIANaL9OQ2mNsAUgt05Q5Za0ywyTkBrrwKMF91koBTHY0MLSJ8/83t23v6Zvi5WoNpeaHW8+LCITFv2LYzLlhOaACYtMFmLX30NDFSJyadR64DzRrhklVKlixejzWCZj4LNKgwXwQEN0OeQ4EwzgwfO3r7nj/k+dTdkQjJAqcQrinhVKlbhWa3CvsKSac8NYamifNa05KZHWSWsNAvYCFwBgDFdLyhMTE65qp4A2gPbkss4utQh+cjU8LreSZTcAuRlot2YG1TT/bOm+Ywo4+B2jBH9u3TpD2h2ike4QSb9BHXQAAI29NdoqUtIkoW5iqWYsqRXqF0Jq3SNJa6z9ghOrRpV44b7Fs+9YXPhf+jY/Uza67/XYe3KicxA0leaIXPsYeIjc/6gj49WhAr9OPYZkaa4GxTOAUoft/G7JeoEFI3PqIsueTMMcn8cYEcBMIK1cdSt25Y8+9aGf/3/anzyrSY0VBSekBrYCUb/fh/RzrhUpda0lLRLSIiW9uEGNoT0MABsA9u7dC60PlUBYXV0tO74FoLWyTos3ArPx5GqFjUsY6LImLCTLY8YljlUmrM7d4gzLxS7Z5n6ykrLVxHlfIwlfuGh4KFEX84FhbTCLUWO3TVIDsbAarRFrv2BVYk3JNYjWOBehhIXAhl7+Hc+dfMsNr7olNemBQPPhGoImAfzCwYG6Vx0Rz3DvzK/W6LsVUwx+LzYvsUkbfD/4e4mBhQqgAPk8n3D2aKoCwSD7vZKi1SjWFmfe+8m/+vnFB+85A1Jx31UhUimQCIS1kMLWIUIJ2byvwgksQYXtkO3zRtHdttIcbohpkO1sWuH9TbnqopWLVIjhXFJztSQX1zUZG9H6uTPaxpqg7b2wVkv1aKYR/wyAaFcGCi5jBjeBuAjg+MpB7Y4epBMPnsDpi2u6vLehMjwSAXAfLl4EmmmdtgAMDQ8DhSVRYs0TQh3o1fquBlZI64bUCKVsKFHiXPqUqGUosarlD7Y/e/Y3PrrwC/2ifyaucvmSXHPHnljseZU7W+wSoxn06gaj3CV4dKf2UT+TH5k07FjMtxM8GgFwwBtzBypNVhWYGNgHwEpx5n2f/ptfuOvBe8+qKEGdqQcbUq0xrLgfslAfAIqEUKtBTUpAHfWkoIY2GUO+7XoFoZ/TPgDrWAcee9TV92LHHefBE2gvf0UxeT2h5aZv5tptonm/CGGXspsXRlAgI7cEZml8jWbXrqFpAFvjoH31IRpOlfc0R2l1s4NhNST1glGkdOCq/Xyud54ApiFOWIcNq1VWK4yiTzWkrEYZnDDUckrMWvQMTMoMUE7CzEQQIQPlBx9d2VrdWP/E86a+8wWGzZ4YRZXuiMHlOyzS2yUgMPib8nmAcWJWCUeMzc+l78u6ROaqrIv3sAjYcWYqp23i/Y7CHj+GCNbmZ973qQ/94rEvfX41ZQIzkSiRZVIWAcGqSZgEuZoigUmMUkqasEK6pFCBiJUu5VoURgtDWm8YyZuk9R6Q22EFEUyNlHPniW2N79dzZx/WIwcP4gWfeUhbc8ACtRRHj+r8ZZb4XHY2/miWKbJM5wAszrR1EQCW2uXfl1dWMAFAtl1MQXv9ko2G0hptb225N4UQ6kBtOKHUGP+dHGqFwEJIU+RFAShxIobATAYpgwwVKPA3J764+vY73/9Luc3PlKaM4o5CyUDVaEcZ2KtiMpUuqfJrgp6KAFG61RGLRL8f1DuewSJIVnos0k7+OfBMabaIBmbWw7O1xZn3fPKvf+mjJ+5bdYxMXABIWElzywUbUhADQMoNUiMEG3SlJRhLqAOaJDSEBobSGg2lNdJeQQCw0dsuG1A6XbKb2/Swf986+DQCXHZHOSM2P0/qlkM/IRGtSsB8lhGyjBZCQGl5ufzxtTiEg5OTwMQEzg3VFRcAqtd0c2MDzXCQ1JLmlpBwWdmOtQprSD319sULaSVWDuAhBilBhBJmUli+48Evnp2/67/8ct8WZ0LuUFTbMgZTzp3tcKmrZcSXrh/f2clh+5QAtJAcX353gIW0rEPFYDuZKJRBrUZR49MO8PzHu2//5Y996Z5VFSHD7Bg5JwYSJAAS3355Hv4DlIVgLcGpBPSLQoEettHFdt5X+AE90s1JezkBwLmhDV076yvxsJvbbk9+RY+vHFS03IKJDACOZhqvG4vL7l4YnN9ffrLUIvjVjSfOndGVi+u63FlXrK4iuPBa79PG5gawpwlsbYFSo1TkA7Sn1hLCFQJIkQJiqlqJEno5QYRyUTJI2ShxQUr/9wPHV9927D+/sWfzM0E4lybJu/TuEK7bBtx7DUFAKRnCAUYGWOqSPOrS+/L13wGeAS210/uKNZVqtCBQ/eqJaFMIOLNV2OLMuz5x2xvv+vI9q2AleJZxPSWkueWEmVSUYJUS2Kj/UqS1GlBz72pJQt3wp8ISpYlq3zEQ1VPdV+/T2PYITVzX0ImJCeDaQ8CDJwAcASaXaRZO/2YIq1Z8E+wGlp2fBbrNfDYiZtp0ZL1KXz20F0aao5QXCetml4qaGK0PUXPLmiIRI8qs2uV6H6aTq6k1aybN1YhJjRY9IwyjrCZBjdW77wrLSUKsIgRDrAUxWEnJjcBQuRc+4zkTv/bCH/udmkkn4iAdyo4N2sR3pg8ShlIpkcgH0uoY4bNBD6w6vkShhNisxct2duoo0TC1F3laCMzjpicKa8+88+O3vWnxy/euQlkTEoKyEotCSC1UEnYBw8J7YSmMWC0sm5rd6lWeWNFPijzp2waJGEu2V8BSsyGm2LZbnUK4TpYpl3UakfHtDU0xKsvoy+GxET1R77nxcse6e15YcJdDZVLbAIh2SygDEZVNnsFtlHB85aA2Jkf18NgBSi42lTvrura1oY8CoF5NXXAK2NqyQnlfsQVgqIFGw6MyMYp+HzBGyQSzlqNgVqTV+YvEv0gAFQ8eUfdgQ3d++d6zv37s1jf1inw1hn9lOmJRHJudilUGc3kGsxQr/VK1R2CUODd5pzbaCZ5QKS3BU7V9YB6Cy+nJbb56y90ffPPHv3zfGgAk6pmHqoGTAEBRVO0kpIVY9c2IWgrXttb409f9owFNa7Sd9xUARvaMANiHffUmXQ2Amw0FgGl/3CMAsPKAzoUTzc0RzWduZmJAO7hyuYSyS5jpyOQyHQfwjMNAMjaiOHQIEwCuvvpqjI4C+wBs9LvaTOvuAKsbSuEAACAASURBVGmi2O6CCqNUuIsiYzS13mR5ECVwNjxNHHISW5k0qpGCKw2lIqQidNeJL5z99Tvf/+ZekbsoWCSsBwR1MDEacqYH5PGlMZ6S0aLjoEqbLV8PmLxBJho4HiqGgwbdE/SOe90v8tV33v3BN9194t6zAJCwIYRBBEDJ7T+izIQkATyGEgAQQ8T1EkRV6aGOHgCgW+RKeV+R95TqiW70uwpcwIVeTR/d3lAAWN67rgBQP3dGAWD2xlmstVqUtVpa5khn2a7bv+yeUIYKQRmA8Zm2jqw4DfTQ5hktzm0QTjvR9ejDX9X19XVcwAXsa6QKdLCV9xQAulQTR0E99PwFFTUVCCtygKwDRyFW3YMU6kZVAUCtluZLmatxzEp3nPji6jvuvPXNPeuZqNQvvvOphEnU4YPMExhkcFa/0j0786dj01ZqLeglIKoi5Lu1bGW6cpuvvvPjH3jzp75y35phJfcQUskJ7AaSVb8WurAoigJIABLybaSS5zmQAvAHcGeou/MUiTaG3GBuoonNTYAaqVKjNlA1n1aGns8yXYTzvI+GVRlZppl7/YQ0kPtc/V+zzK1Eba0RJq+n1so6ddDkyb2jdOpih/LxhO3aBZZ9KYsaHu6KEe1yPRkynSHm+kU1Ypz+SblnuiBOi9QIq0lYjdq+0aTGanNjkLImPpgoTCowICGIIWUhsJIBAGECi9oCeNH1z7nmV26a+63EJBNOLEuVYoodTDDYj14rodxKpfwMGDxG+H5knsJcm6c/WN3hzCuqzACtliqFmfXcFqvv+vjtb/7UyXvPVuE4C8sOHEkBWKhYgkBIU7iBZ42LMhdqbQ1GClPYQsiSwNZrqeTd3HICyylbtmTZkl2vdS0XZPfSkGxuWmFYYYyIgZU1PCzAVTINoDkzLY31ZX3a6Lr4JPuq2S6zxGf3QCKgmoGIMlqcaROWFoDZ5wLdIVrb7NL5x9aRGAOLswRiIKmRbCqP9JhodAiqOfVzS0OmQVCi7aRHxvbJIiGoIUbOSsJWldgQgZgLEFI2VOQgVjCIyFohZhAMU2IMCSnYpRASlIiJ6OT55c7pC49+6nlT3/k8Jt4TX12ZphGblvjvEUvspmNoB2h2c+FL1imPv4N5qBLQIaswt3b1XZ+47S2fPnnfWQgTFGStgBlQSwADWrACpIYYTFaJjFpVgbUgQEwKJUpEREWEpW5YqG9VDUs/ZUktS8/mkpJRI5A6qXKnq3m9KZyymrSnVKxj/+S4XrVpNRkb0eShr+LeB4aw1H06ML4GrF0DzTKl2VlaXFzcBSqXCSS6gJG6GMBSS9HybXL9QW0BwFV7vO1slG1FjZpy3VEj1ROlNNHtrS0gzXWoMErGZcnBsBJSIUqEDGthnWeRwJmyJAEKP+eTMGniNZAVURR2oJ4WBSBMd33l78/+xrH//OZuka/GLnbsSVWz7a6DbQSqndMhvg12zNLrgI4CYhFerVUrTVj4XgAxnBfTtfnqLXd/4M2f/urSKsSZZcuihpmsOyihAAq26kS0BUmqMFbBXhMaUiqMkLVaM6w1b7q4poLEaCMxSkWhQ+ke7UEEW0An7yowAmps63q3U2F8pUp8aE+OqlPRi8DSgmKmTaSK7GimwbQ/EQA5EPjQdRb9Yfaj7iStmRYwBRzCIZjOkNJ2Q2m4oxcb20qPdrXT7+o2WaFmQ7qFUR6uCVEqSIzWbaE5qZBlhbUKUsl7VklEC1KBWCWoWJ+jZVUUwduAcQ+uQtEOREof+8p9a7/90Vvf3Cvy1YGswKhjHVjCrmA7P9+hhRAlielggr9E3ykhWWoi7HhUDd8t8tX3fuL2t3z2K0trECXrFbHxQDIAUCMlEU0AJMpOzLEoigRgUUqNkLDCsOakklurZAvNSaRvWcnmSp1Ce8M16RYXFEVfKU10D/aA91rZ76+JtjeUmw2dnJzE9NS0E9DHj+MIqg1gsqVWCZnL7ReU7PxgZ7g6AzA3M0drSy1ajD4/+YVVPXRdh3gvlG1D92EMj/V6ulHfVPRTbaaKzQvr2tg7pCiMUlJXykX7iSp6uRKrEKWUF0pJSkJWKYGaniFrrJpErQIkKsTEKiqWiQ2sCIwX1EaMS5ZRqGVWVaKdy4FDx+sON7RMxwjvy+6nAZMUYBGbrljnlJpItUxYo/i3GpZvOyBZK7Ao3FAgU6VPsihgQP1CwaREEKuqlKhACBa5kBgFWSWQkBUlYaWEhGq51PNE84S034cdgkjpAddYiXrKVCj39usFsoohVuZJNZ2LerzTl8Njj+kJADj4NBpZeUDGZ8YVC3OULWQKgPSohkj0ExLRfu7PASkDKFu4mQCXI7KxvEHHjxzB4V6di3MbVOztkDRHqX++MFYTXu+epadc/RSj3Zwu6jZLX83oSMPoFrPVIpE6uaT6IohoNcJqUvWpHiAnokGstjAmSRiiTgwUxEFMOwD5xBkAP/C0Z43/8o2v+s2aMRMChZWqQ3dqlhJEiD5XDPw99q52fj8W3Du1027LOMuJUi+e/+Pdt//K50+2z4JJjTD1bSGokSYAeoVKA4AlCKBCULHWSMGFpUKFYKRmyOakwkKWDFnKRQqTW0rrli1ZY9l2qSPdguyeZkOSgi2zlc2LVnhsRHi7o3Eax0l0XLUnRxWj65ItLWhYoarRjMQTzUh0w9BPF2REiqWWYgFYnBnX4wdfqgBwot6Tk2OrevpiU7ECGM7FUCGjjWuU+l3d6Hd1Tz3RZppob1sEaa6ckuVeMF+OdnNDlqCSk4prIBWQSqBrW7jGteQ+J/G0XgBAAatWX/D0777ml2981W+mxkyEe23Fi/9Kc4VKFMeJ7wPxoOjzneI7Bs+Ai69x8nwUCtjFRKacTLz2BS9/x/ddNzMB6010AqAAbJ81AWBtAI8RS0YAlVRY09TIcC2RXFjrSIQgQpY1r6nAslJRKBWJIs2VUZdmmijlPcXmBjYatdJ9N5yL6ZxX4DSSsRE9PHaAWgCwclCxtEbZzBxVK5Kp2iNzl3K52XgF3D6ACIvrw44NbnkhsLJOh3EY06gCUbSnodTt6CZZoUaq3E+VqSEoEqWir1QYJdQEtvABnpDPm0jNsFLqhDUJK5ERCHnBCEl87ANslZg1qZFaZr3xuu+ZeOMNc++oGTPhOkkunQ2PhPPOZdGDgUHZEUDEABhC1Nr678XHxw7AlgqJLl2jn3Iy8Zrnv/I3vve6mQnrpyqIWQvuKxEETErsB42410RGikJlGyq1mh9wSIVIhfoinDRs35JlEun6ZT1MVgwNyebeYaGz20rDdeXtTukAJReb+gy4+c021mT2RsDvsKuZ30OSgiOwi4D+WgDylw7gaKYZ4LY2WQDmwnqh5VE9ce6Mnhxb1emLTTV7h5S3NpSH6nq+11EmKxv9rlK9pwyRXq1pu92OMIlw0bdEfvTkjnUKIVuQColjJCpUUiRCMIICIBanB5QVYrUogBumn3XNG2fn3uGYJ9pKV308CKjypneKakTR6QFRjBIMO70tDQ1ZTuZWXlkQ5oGlBoKWqKLZ4X1izMTrXvDydzxv+ruuseIchYTJ6R+ooO+u04q1oEQIKsT+2kMbkToWaqRCJDKUGu1SR5oQoVqilCdK/a4yWaFGTc+dPqW03VDTGdLpUbcW7KFzZ/TI5KiW0tlnX2QBCH6ZyOW2/L38BlNavTh6NFO0Wm6zasBR3REAk6N62CfZc6fpWGh7Q8e6bm6M+oGF3PwYJSOOXodqwknDEonUDWueSwWivO9eQ6Ug95pSH70W17gJs/7A079r/I03vuq3EsMTVgWD26xU99wqMwtLsFSe0UCKhlbgkktex6wyuH4smKfSDEaMFzOdqr9NkwoKEVhRGDYT//b5L/+tG57x7GugEesKqzWFBbMmxl0zQQWF1z22UCKRHCJ5SpZJhYpCqSvCqMs5EqG8p51+VzepcGmw3Y6ODx/SFIXw3qae3FjVMHURct0X19oVSLJMQWUi4u7087UARORvqwCA/HR+q9VSLK0RsIjZlQcUx4/jxLkzmoyt6umL62o655WbDeWhulKjo/vr29rJNy3XUqU8Ua6x7W1bIRKhJFG2bHPpWzZk81ykBhWYuhaJH2FQQZFLUfjRVhRiofL86541/sYfmPudxJiJwneku3mJoAgsFExSbKIQg2oHE8V/8yyGyBwF4IU0kFAu0T4IcScPO63W5wfQWfj6qiJhM/Gz/+JHfuf5z2hd4wYIPLMYIR91JvKP1DNOUrc51C3X6alQtybOdPV1m0T2UEMMDck+DAv1asomFx6q61rTzX2dvriuhx909W/7DaZmbwTmFuAWkSLaF+hycxW+7L6wcABHVLrvs1mG8fHn4uZrPqx/gp+kI9eP0MrIECbXBK2p62ldm6ARxWq/h0NDV9P5wkDRp35qULNCZLrQxhDlOVHStdRlQcJOatWlRkBOpAnc7RsEhhMqLCCJi6eZlPGiw8+ZfMMNP/p7iTEH3AqKSOPITvc7Fs1VCe569Tr2wqr3pRcX/TZOHamyF3eJTvtzlOEE7FI3/zDMe5596Pp/8Wjnwt2nzq9eFFGhRJVyVeJUCimsQIXFASpn61/XhOuJ9Hs9W5hUu1S3e4SUJVeTQhg1Xd28oEOFARfbuj8fUb6wrmONGvjQJL60P9VZP8e52GzrUgsAtVSzTG+66y5oltH8/PzXBsjX/GvI+/axjbCNf7lzw1KLjiwvE44Ax//+YZ6+yi04lGaHRs4X5ksAoBu8T1Nupmqk3yBRwzLMLH0xqn2umZpRzVlBLAVMyqlR5OxyhhKjKDixiVG1/EPf8b0HX/v8l/2HlJMDTnfELBIZmchMAQ4IBMc0TJXXFcpOnlaPjohbEKIbUgItBlsFsiqvqJoHA1WAi1dfsPduDBhEgIg98+ef/fC/++iX718OHmkhhSVDtshF6kN1m3dzS3W3eJBsof1eUdBQTbjGlreccOZaqklu7AZZMVQIbTe86VrXZLSpyUa1vR1WHlC0xxWtBcXMnFuNevSoX1oaNdFlyuPtE+10DfkdiMOEmp9km5tpK44Ax48fB67ao8nYiJ6+uK7cWdfznAu8HqJeTTs5We53NYg7hggVRnu9LSESgTVKde9VhKmOXIWQSAGVl7S+b+K1z3/ZHyScHLCqTkcE8yVSTqJaiTwxRMuSYxNUmrSgg2KNVIFmMHdI/ELCyluLU0Aq84aB78e3pHQmzOszr4esCgq1sKogNgf+p+//4T+Yfdr3XENQKXyIo8hFyJDNcxXXRiJE28Ip2aHEKKV9pbyvVEuUa6kyW7nY21byUxbcHNblvet6+mJTky947TM5qlg5qJgZ17k5YG7mVqomT2ln2vllyxO93VN1vOCVLThBPbJyUHFwRLH8FT3x4AlgbFWTi01NTVcmvB46j1yoUdPNvcOy1bkgvLUhRCLDw3Vh1IWKpjJUyOaaQwRJoQSVIiELYX3pzHMnfu77f+Sdhs2k1dDw4RaPzoBZjVJWUekOjcSulmAYTN2odFKUK40KGIG9oGEOjarvR8DRgX+Dk7ExGC2i+ou71bcDkQCgyX/9z//lu276Z8+ZqBnWIqnZWi2VWsJKCdl+3+keppoQifRTst2CLZMV07kgm5vrsklWxho15a26upjPRZ2+2NTDY27njUbJPG2dw5zPoHcp9NnR0OOPDx5gl6mMrwUhVQLN+zveeBAttpwDuHjkCLCyrjhXnd50hhTNbcF4n9gCsIBgGFvYwjA1sX6xi8beBvpbPdQbrMgZYlLk/R6lXBBMIq/4zuce+Il//pJ3M/GkY5qqI2KXvOws98JfeZXKW8UxLtUwoZmCJsEOLRUEcZhY9Xb9El1VHjMyaQE8g/UI20p5Lzl4cBAYYjBo8ubv/5fvZk5/9o6lT57OE5IGUiFLOgTrhHQh2qtBqCa6pyChPFHeWxMmK+cv5JIMsdJwQ00n0eUL63r4ugOKB4ETvWujavu9yFqtaivD+XnCUTzh8ngaaPB7oR28Vzbnd27196EiYBFHJq+nC36ao48a2+Y2SWeEdKhLYQWr9nIaru0xNlGjeUGSq0FSIynU1IZrRtHnlGvmpc983rX/5nte9CfMfDB4MGVaKqpRHzRPXMW4+NDNQJpG+K6i2tiq0ilxwlg4xu66qMyRjkR4BBdopIXCnj+KOM118PZVROF2TQwRu/xX99z9k3/3Dx99uE+p5Ft9SwnZYc8+Pa97tuts9zZS7eQdS72aGr/iFKtAevBqOZ38oxzGYZS65ziAF63LHICFKOcnOwpklMX7VTwuAz0pAIWGcOktPko906YQH5pdatFiuM3TY5vkRHWHTl/s0vj4tWzXLrA2miS6yc2eGttMeLNf0FAyZATMDWW2+baRZs3Mzdxw7c3PeeGfM9FTKvBIaWaqDkPkVtNAp8eTmtW3dxG35V8Hvy1a3XN10HvSEgjhd+Fz9egYyBEqhfvO2lDJQPHdqd393xkJs6rIIx+676M//qH7PvYQUSpunkuEIULbIqZGtgMrTE1hskLdjp5DX4CuHNp7WMOS5RM+YHjcgydbWtCsPUdhhiEsX9YqeT5U//GB8USLuklZzGfzdDTz66VDvMBvAezuweAW6T917AD9g59wPW0bPD48QnH2ol7c4matQTZRo1vMtRE1qsz/4/e+6Kkv+87n/yUzPyUIzxI4kdnSmCU8zQRT5qikQkk1+qOt9AZ7M3pZ8dQAo0Rso9HfBmbvB9hoh7fmm1xV/PeoZLw44cxQ2A+R1RCRijzykfs//eq/+fziQ73UbdmyvS1CNbJ7+6l26puWaY/QcEeN6QtvuWjzMh6Qw2PPjDyug4oZHyxcACrwhBvyIs5FfVzwAE8SQL4RBx08n/aaIUPWniPMAbhjlHEEONyrM3ACbj+hDuW24dJfh0dIkPKevYZFDNvNbRYYlkTMzz7vf5h6yTOf/34iOhTc9PAMuFye8vJ0sIMpMhGIuqxSHZ5BadCsXdIUVIElHKxMYQ0A8n+36tZ8XWLy/HP1ecWOca0GCu28W6PbPNxtNIWH73rgCz/2/s985BSRyJ480V69Y6mfaqfPlhqV6TKdIa2hL2GzBABooyUBPJm/W2F5G1D1zKNUjZsnWJ70TXd9hNqdiBSZupuPHM3gYgkAZg+6vYRO1Hty4pxz7xM/X/bo+D4Jnhn3akrBtU8Tfc0Nr3zKi5/5/PcrcKgQ554XUkWNC38PLhXHRi7iXI3ysD1LHBPamUUYu/Lx/JfbaDOgMiAlmMhw9ZXXpvBghg6ApzKxAfSVDqoYCVVdUa2A9depAqgFNETYC8dZ1950/XPe/+rnvfTQVt7Tbbog1Ei1M8SWx1wmRADPsgcPcALtyVFtoyXAIuYW3NZ1WehLf1EKxXw2H+YtnjB4omM8uTKQdEZUWoqgiRw9Vkn4bQDTaHIIMuYr6xx2sxfd5Il0j/mx73/Z1I3/7Ltvg+LacL+JygV311RtRFkJ6HgTbnf1FQPvxsOBcQJblWYPLuG9ZPKI3WLBPGi6QugAA6atAhXKDRbKoR2EPFGZhBar97BeldklOnn2IUNGjVtY8/Bnvnz/K//yM7ee6uRkqVdTHhuRtHfems6QAkANfSlzfJZHdfbgA7rYHtestaDtmVtp4eabBVlGOJqVt84rm+9JlifuxkeFohvXl40TtNDSgmJujuYwrgtLD6AxeT2wsk7J2IhiZB39fo24OaV2jAlr50GNmv7E819+6Pue+szbVHFtdac/KZknFq7VOYHYKxq8+optAkh2Zgqi7DMfnvCMiqAG4qixRq0cNI7Cz/fHgK7qV+mm8ButhjwA9Xdhji9L3Q65CgipMghKAlYGoLAkYBiia7/v6c/8QL1ee/lfHPvQqc2xRHi7o2CA9zZL0YxzHZcghoM6PrOugE8SW1oKttifXmkgbPgky9fFQPFvPQuEiwcAZBlcUtJSy918F8DxlXU6PHaAxouET13s0HJzm8bq+81bZ1819V3XXPchhT51t0284/kt8SAI2/sGUAQOukQYe4YKKRgoQeRGe7jHFkjB5d3/qtuR9/J8TaGopek4EKVmRNqs9LDKAGR89hhU8WcV4IhJJTB6jFIomFmhSkzspjzClnfMMMBD9z104qXv/uyHTxpjJU0K4c66NpIDUj93RhuTo+VavsWZtg4s0TmaaXmx32B50hooKk46OBg7olXF3K23ctb2cypYxMjKA3o8+tHK6Lry3qZOAHjrD/7Yta1rpj5kIU8tyrC+ILzOfeDQiehqZn3nTPqliwArFrg0ExEDTFAyDfl9CZnBROjn+dp7P3Hbr7/n7tvenufFmgNuteEl/PtSNpRGoNJXVUZiPG0Sxa9c/bzjWCkwqwohqFUhAcr2sKpahHQQ0LXPnHrGX73hhldey1sbutxxt3EK7Xwc7o6Si8HrCtNQwWzRDinydZZvBEBVKUU16cLNN0sWshfb47qIWcyuPKAtAPUDVylOuZ/82kte99SZqw592AJPdaF8RS4yME3hOrxKr4j3OSzHOg1m/FWdd2kyVzX6Iz8ADkDxXf9yW6y95+7b3/6Fh7989v7lrz763k/c9uu52DW3SzxKEDEiz093Arg6gWgQ5juATh44CgqDwKq7SaaIUpiqEUALVeQqZAHNVZCLRSEy9fTxaz/8+h/8iSmEDX68cMbKQZ3z2YVYammWZVGaque5J3Bf+CfS9d+MEjl/HvDz85SF2BAATF5PWFmnaTT5V370304/9ZqDH7GqUxXDVA8A5f3V4xgL7ThhLKIBb9J8jzrJERSa+055M1v/e7cTajAPTkTnNl9758c+8Ov3PvKVtfgCn/WUp43/zze88q0pJ+OuzigncaubzUVenpNTVbOUz+SgRoBIRWIxa1bxg9hXITCzF9YcbvFEPmZ06szG2ktu+cv3njz52Kq6dXtrMnvjrEsSG1xh6u/OoN8UEH3DAIq1D+AVaXRjXpTTHMDhscfMr/7wz0xd3dz/d6I6FdhGUXldsUaoxHAlaIEKEMFNDr1QidyqG4LmCdopfMa+E8Itk5gIhc3X3vnRD7793uUTj156pRbPesozPIjS8cKLfOvcbFiJbvNNKO9WWIKHKi+sujJA3U4oVIrq8hqqM8d3zg7sZ5hdDiO7aQ9DdGr1wrkX/9L7f/ekE8/AXLxEOcvUmVeK7/b4bdVAAByCo4RrugSTYZ+0yWX633/4p596VXP/34nKVDBbLsVTq9lpjRkJl7BTlbIaxXbC55E4DUC0Gka2lp1SButQLbfp9/O1d378g2+/d/lLjwLVClhLqpZUAYN7H3lw7T0fv/3tueRrDnSuQwc2jApueanuIw0WmTn4bE8t7xBNUU3d9jouOVohXheV+giKQixZSDBtKFSnrtk39nfvvvmXpgC3TcvCwgKypWp3DVC4v0g0Or/B8k3RQG4/Id9B8xkhyzRrtRRzwGzLmbDWykG6enjffxWVqVwVlWiuXoec4TJ/pmz8SjvEgcPwvkrBwMCKCYmmCoJQLuNGVE1c9vv52js/cdvb7n3kwTUQqxVWK6qGmIwSGRUCqYJY733kwbX3fuKDb8vzYs2xWJi7qo5dxXy8x4eIjTxwfBqtKkFFhcTHgt3f3N4iqkrlezeYyJtMLVwaCBUSOR7Qpw7vG/uvv/HcV+w/vvKAojVL7ei2TeorVQUqvnEL9M0R0QqiMJDcViCu4ktrtIhZYHmZ/v3P/fjvCvDdhYYIswNPiCjHzFPmzmDHs1b5z4PC2ZX4t66znHgNXk9pxsJcEwj9fn/t3Z+4/W33R2bL+ERft7WKZyOtNnu655Evr73v03/1tlyKs4HBnJaqxGAZWyH4DVpI/XWQh3jplYFInd4TQgBR9VDfRuragtQBCbCqauEYvIzaA9996MDht2LyeprFLBYWFgC/Ybg7Mu3knm8IRN8MAFGoU1hHlrXbtLbUonDLoD//1ddPEePfFeJc88A6hQryykUd9JoicxRKENghgBEvyak0lJZiJ7RXYJ7y9tj+kef52nvuvn3+3uUvnXWqvRKUDkTRZg7k995hUljgnoceXHvv3R94m1U5W85bhWPHm3T5I6r6JVbe6/I2y3lg4iZSfAA1sE94kDqZQH6hQOmdBRngpztQwLWpKn7hvS/6+RsXFxeBVkZhiknL8Htpwr59Gkire6uWbnEwX2i1dLHddis4lpdpxDT+qNI8OzMK4yzBsBynMlMDXhoqhgq3Ixi4JQGc2Qod4IwEIuapNE9RFGff/Ynbj9678sAaM1PsjUjY1MmvsC7BExUi0vtPfbkEkQnHZtawC1kliVHdbty1HtQVsoDu3GZGUAa7Q33KdlZFxNpCoV38ciEK7VwfGnorrneZEXMzc4R2myi+A7N+Y8wTytcNoNLz8oY9JJkpALQzwpzLVPzDN71+SkE32JJpZIdQrjyxwW1SMMAq4jkueGzhXI76EbBSzXFF9RxgHyJYsWffffcHs/seefBRKKn43aGIHAux3wFEVBQqWr0mFREl8qaNSe956MTqez92W2bFnjXEMG4hSwWmAMzAnl77iPrpWxVnaEvT64RTbMbDtUYBU1WFDzYqYockMvE3vvNNPzkFAAtLayULYX6e3PZKcCCqHNavq3w9ABo4mRLcujEfpJrPQGjNEtZHeXHyeto/3HhZ5fJWDCQeTCXTeCbaudJzICgYvhOBrNRBZTwlGIfBWyWFu98URXH2PXfffvSeRx44a/nSGIiIZx8LGEo0CCJDiRKJGhjYyNwZY3DvyoNrf/jJvzpaWKeJEiJ1cSWHZ2ZS9huXRuK4dF5Lsez1koQEOR/IihdJ+n8OfCI0oA8RbhTjBl+tNvpyTC4Tlq8nzMxRyCAFgHnN3CxPNdq+LhA9WQANRhA0+A3VzXmzmTkCZoGVB7S1sk5Qebn4ERLMUbX4L17lGUxW5IbHpizSR5U3VkKmBFSoZrkLM6G8t3ph7dn3fOL2X/vi3uvpoQAAIABJREFUw19aA9yiuBJEKmo1V2MMiKi8NxaJqOSi5V6FsNViOv9bK6T3Pvzg2h9+8vZfK2xxlkFkyJsy5zdTVaeyFUkBdebKX5domSztNSCV6+6dqxKZMPfbgcUAUqb9qgDKTDe0VtYJBx9QLLVcrhYyUJZRNu/q8GSyD3crT5qBSmc0tEQATrtNczNtmvUrV7G8Qe7GCDQVOj645wCqhoioe8CLij6vADX4dzciqWSckNEXKsio9iQsivzs++7+61/9wiMPnA07v4qIwjpT5B6JAhZWc7Waa/CfjPErMGOhzaQWEWMx6/3LX1n700//9a8W1p4lEBlmZYBCSkbp6quL8cQ9piD1CW5UrT1zTCT+F5V36VnLfVdDFNz6Y1pREhVSwrPaj23SEZ8lurC0RpiZcyk3yFwGhZMeCoBiXftEy5NO5wgNkIXVGVHEeWGpRfCz74fH6vwMAAqdknKMw4s+qIWLbSiCq+QFLwXjDC+A/c3vEIaI7y91XoWbF4zjL25m3aeElmbrP33qw2+9Z+XEmgEIFm6bYREyxkBEVJmJRBT/b3vvGiNJlp2HfefcG5GZlfWYqp6a7ir3cIZ0z4iuJE3ADVtYwRZLIH/ID9kGhNIPwTYg/zAM2f+9JAV0NgxY/iWLMGDYEmCDhsRXQQJkibuESVu1MqHdNdlLcsls78707s7s9FZ1T/VUdT2yMjPinnP849yIzO7pnpldyjIB8Q5yOrMyMjPixLnn8Z0Xs4YMGwuThaxNxNRCCP5ROOMEZlIAkdmZTZl+74N3j8n+0V/7jz/37/3XIfBrSmwGJUaTroF58gTBpY5vD5K5Id8yi/sESm2eNRGaaQzeLJKdwUyJjLyztAOQRAg/9Oa163Tv6JSwBeBo1/b2h9jfa3rQAxgObSGsAbTn+dlCHN+PBHrGLR02p5ABwyZcAQA4OqUH7z7A/3NyQY2qETOyrHMbYKwBHy3rKhfF2hrG8wZRiw/k/bi4UZru74Ym9aFJf6hT/eH//E9/7ed+97vfOBbxMQvC6gYDs6k6Y1CWJKpqClOFKS0+p7kBrYCC2dUcs38XeedYENnvvP/ND3/hK//wr6UkxwGg4C4+NXPAQOTNQjPzWNPYPUsVl7o5ZpbtowbFFjSZ1A3thLIh3qj2nB5C7r29AeCjSxocnRK2Dmn/p/8zxv0BtdXFXr5MC0G5JjT1mSTRZ2Gg9ovagvssfYY7I2qS6XcPDgAcYLC+vVCRsU5mdrboVje80UD0Btfn0piHmEfgF2Njiy78YlpEe4qNleHGa5Y89Yf/y1d+7Wd/7+gbHxJnA1jVAjDvs2hsYmagoKDg7WMWH5wfzevErZEtjRnM2ZzJPYxiCPj6B986/jtf+Yc/m1QfL4CN1sbjiDL8SkCWAHOPk8ygZB6ub8FElzQg9YsnNBXDeZlpK42zN3eWvj6mN/+16zTbuEG3Ady+B+AnPUQJDOdtXBZd/O9jfSYJ1IBgd+7caa12z247pt3NAQG7uHj7bRrs7PLs0UeEtwBvfTcmNXvfWulipM0uc05yfW9K8zwZtKGLOUDYGNTP0KtFlQkAE1mDCAdiiKTHv/CVX/vZ3//uN4+DMpkyEZMFBIDImSi76I1yBQCEbJuG/LDMHIFV8r+w3PjSnvssAGptJLXff/itD3/xK1/4OTV9HIgQAMquPmBG3GhoItMFDW1ZQrcWp9szRm3mdkM/z1lo7okzo2atb1DC+/qj65ROx5RWrtG9P/yA790G8EuHdOz3jYejVhK12UHDOTO9cMTT98tALeTUirrmR3c27eBLBwCAe7f9mHRyQTiZkJ6vk5yvkqj9Qb44m8P71JJn0Uh+Jlls0QNrjlqY4bG48twt8qpOQDQ9/l+//Gs/97vvv/Mh4C2CKUscYveoFKYEVqh3CIO46dBiQqZGlnvzGJlYbZHd6Bak/Dk1ye9DAP8NyS2J/dy+9sE3Hv/il7/4s6L6OFedIjBbIG5BcjIjMrRqqKHMsx6pg46LCWmZFtTQs1U8WQ2ZydfXjyLr+ZTw/nu49dYtr5S5DRy804zv2sdwZ0TD4bAFGoaesWiZtp9oC30aA7m0NZobzQ13jvZyrs8uBhjx4OiURo++Su8B2MbbLP1Vejy+osls+lvcBhvdbXw+KV8bwmAeGBWYCZqI9TOnZC2yjHlFp3tbgIg8/jtf/sLP/u4HD3yORuNh5RsOJos6/1vTh8i7oeq8Q1iekOOqSS0ymYC9LZ8HLZSYLCIArKYs0vzeoooMxPS1h998/Ctf/cLPiMrjnJZKgchCaxM1qi1bN+1Na1T/s4SwBbfCLA/SyXcre51GIJxPrr7w4SYg/VeoQsnp5MKPOzolANjFLjDYbeSOQzHZq85e3qewxye1uHtGdBmGi/PD8iiXXQDYOqTZxg2abdygN0+u083VMXk58/u0gcAHH4y+CMOZ5YtsQgpNeoxHh58xFM2dWt+ZTTJ6Qz/LzVqI2DxskBPDQjARefyLX/3iz/zuw29m5pmHIMQylqOudkTnKDOMTMTVVTB1eygGBaAEuG0kps17wiSt3ZOZLSC4ZEpz5hERwNxbu/fe/Q9/+bd//fOtOiMiJjZucKoMRzRiaC4C3P7J1KLcfBaNnZuna9tiPZk3Qdbv/vV//Le/IOJ1eNKfUIWPOJ1c0K2NGzRY386DlHedfUZ7tGARZafk043pT+pQlm9w/o6mN1CjvgYeac+aC97yd530fJ10PCXrrZB1Z/Rb//dvnV/Vk/8pI8HG5IohWwv55OY3mjKDzMX5/AmRT2BrVBabq62Yva1f+uoXf+Zr33XmEVXv9G7uJYX8H0wNReGMwoVJZpJI7N3BYEqolcSUTExMjFDnjmHB31fNxyIb3WpgtmBZsuUVisICMUESYoj4g/e/efwrv/3rn2/UWQBRJEYwx4gC2jwlam6fISfsOXcsVHWSMQyEuWHeTgIC0bSqfnlFAttVn+xySjpeoUNsIa2O6QEeYHR/hOnRKXlX+mNC0+IFzYC5Z8X+981AWXzlLsz2TMd6AJ6iunVI06NTSicX9B7eg/Zd+lhvhbTXJ+tU9HRW0f/xza/+LUA/aIRPW7rLjvgSbJ7U2IjvefsKW1BXljMIrfG0ApEllce//JUvfv5rH3zjw+aMvQF5E0HX7E255BFVI+Js34ghmgqZIrIKQn6YAkHjwmshU8nHCplKYyOF4EyVmRDMJoBCUjttx3JA9A/ee+fD/a/+2udF5XER2CIFi8xtELaRJFlcG2MhTSRXBRMWcrNhFMBNVqKnlZh98P73vv0rOq1obVqRnFyw9aZ0ve+26Zsn1+nWR5eEnQGaUnTcPybs/yVgmOXQXEX4Py9ZLxdPi/hca/eMaPevDujgSwC2DmlwdEqzjy4pXbtOFUo+xBE2N19nkcB6csHWqUgt8lJl4b/8C3/l33xjc+vv1aoQwGoVauve3esyIE9LnHOTMZQoG8e5a4UxiCITCmJLKh/+yle++Pnf++CdDx3hMzJTImJrbprC1LvCiwkAMFtQJmeS/EvKBs6jpvJNj8Fx1iQJMc/tSgJEMkKMsGaSYvTfbRufN2hjZhxRttjEP3I6yI+//i+/9hf/9L/z3wbi63VTjUKwpEpNAyrXj3NzIitvA9AEbI0JFClYAKjggIIZ3/vo0V/827/xq78V6Ez5cknH3XOhpdJCWNPjmPRmruBomi548eG2NeU/QwCjnR3av3/fMtD4UmP65Ua0y892DZsnX8r/5opTvHUL6WxMcjmhTbzOdjkl+94jsk5FyyvrvFQsBUXgn/8nv/zl89n4b2RPiWIG+iKH7H4TUdbljSgO8JoolzZsOd+G3CUm1JJa5hEkiLjrQzn9wiVC0IAIIVNwYYB3QlUVIZgKk5CJgExFoETQIgZXWRR8tGQMSkbSvhdIoooJ52ag2fhelFKN8U7kzJMkeXqROjP8wffeffz3f/s3/itReRyJETkgmKu0HMVfSJzP0hbI1w9EECKICmL/lxkFEcbT8X/3P/5ff/8ralPWqkuKK1Yr2K4qsu6Urp9PSM+nBAAP3n2AWxs3CLjXdP8Gdjz5bP/+fcOdYXZ+Xy5nPskL82D/IsC057VG83DFDZp+5xHr2jo9Xn5qTQ+g016frNMjm9Y0xhhWlGTG/N984W/9/MX44ucLZnPmYQtZpQVygrhnMm9xEogtspf1RvaWJ5EZdV1/+1e/+ust84Qwj8pIHlqHbJM0zbwVUFJnKlBwZjE1QlBikRhZSSGiJAWTkIqQkjT9CkkhQHIPzUgK85kWQFBKQaWBBJTcYJd8DiYWQ5y3NCUjENPX3//G8d+7979/Pmn9rYLZCnZVVIAoMlskokh4Rj1FJkQKcFo4OFkQoaBgl9Orv/k3vvDf/029mrIVJVmRCMsrsE6P1ArWD2ak4ynV6HKFkt+8dp0evPsAt2/fxi68PQ/u71sTtW+Bgk9wxl7IWm2lhdk8yj4aeTT3/jHdPnyb7m17D6CbP/yjrOdjOvzwlDdeK1lRsHUqWrbIqbCgFtiMuYzdcGUzLjXEv/Jn//3PvX3jzb8O0E1FnmuRYzANvuF2gEMcHtnOIByxPTo7/ge/8uUv/OL3zp6M2xsCtOqhZaTGLVc1H/VXK0IEGVlqXHNVi8yWTCxPHAAp+9gpJUNKQOnTA4MyQcxHUjKTzww08gF4RrUqRWICB7Jm1mvDN8rUMFBgJqnVQEYipojAf/FTf/kv/0sbN/4jgVnKNWHaYEFtENUdjwZAjVk6E+x733r0nc//3S//g39KUK2UhKijnFiYVAOd6bgmoW5pfFZrQKXF1rpyVmV9jNUD38cKAHs7f9X27983wJtO3aG2/cvHWOllsqktBjcz0N27BBy4tDp8m7DtZcpp5YKq970LmWjBYpHXOktUT+oguGK1wJ3YDYopa30VtLPEk2RhuVeGP3V965V/98d/6j95pb/yHxKF7cUKUuT4qRvODbbBmFST3/+d90a/+L/93pe+Hng+aKVRCwHBVRUAgaAZIQAVg7E1qqZlqsw4xGxRyVJKQFla0pkVz8WZjY2AiJQSuiVTqnw8JcgocEmpqgnsjNRMmk7qnwmcmYndZmqlpVqDYEBY7Sdu/ujmn/+JP/ufrywtf05U3c1rQjZeuEDchmsYAM6fjp/+3d/8+j/5hW9+752zmkNi8XkkTR+hZVL9iGdpjZf0YjYxplrDyUzD5lrbBua9jx7b4NqyjTDQXRzgYGfTmhYwuZK1JcNnYaDG/Eb2vgh373rcq4m25wJBAKhQcr3p47/FIq9Y4FRIkMsJ61IjfSxoZWESLBShDGbEBVtQsWDJwn/6b/0HP7V97fpupyjfLjhuEfMyAUgqj6bV9FvHl6df/0df+8e/cfT0eGKcd3xeoWBy1ZFvBquRkiXkoDQAmDojgRVJIDCNoQEKAULQZggwVPKx8CnJeRhwazCLEYomEhEBVQoQBgWCKrXnx0y11hRDwc1nAxvBiOHDh0h4nrwGJIiygcl+/PW3N//1t37ic9dXX/0zRSiux8A3QICIXSZNRzOp3jl6evx/fu3B7//ONw7fe0owTSSp7R0tLJXMhCDK68vCxxcalpO23TzwrBTqxhv64OSR7f7kQNtu9U0Dqjt3HJF7SYT+E72w4V0QMMRoNKK2D+LWIc1HPXm7lsOjyBsbgVUCL9sFn2ngXtSg4ylr7IZOsKChDFprKNjCFDWbMUeTaOIjniIj1EYcInFEwWbChgz3WJ4hrz6LRTAf0vbMypJFmpkSptblwoTVJPmIhEbiIJkSkyGoYUZGoVF37NMB4YOQCwAQJRQFTDITtTNcAyUkBAkhtioskCXhF0mjwPzC8/f5YT5QThqGVjEhaHSxpZLnxRfJVAKkKKKmKs9LhWonktQw5QUpRD3VKpFwh4WvzjSgp4H6+t3ZuWyiUu537XGYKuKl4mTZW+C9vW34H0a2t5f7J+Za+pcx0Ms61ROGwMGXhrQ5GtE+9gF6jW5vlbRxdEry8BFVN15nPR+T2IQ6K0vBrma0mpQ1TTn2+qyzKYMjd2plM2UzZqCiRMKFhcAghhAXTEwBwYiZiTkGZqssGIORoZoEJSamQMpAACvRYjutCEBZTRRmmidjMFsIpCYGUh8YHpjMjM3NXDMwrJBg6ECJooqaamANYEvBGayCKmKwoGyJOGebmAYjAGKBDJEDUlK315gAZkjGrMixSTAziAPUe7Xk4EWzVxXG8DYuqkYUAPF01RAI8CQSC8REBRlzATIxBVsIQCAfKaq5DIkDGVRBgSzNBAQz1o6Nu1G7EsDFBT4qZlimrq1druDapIdXN65hdPwEeP++4bV/g+5vHmN4/BoOvrRLODgAEb1wbupLGcjuGu7ufonu7x7TLr1G7wE4uvwIr6FPcvOHCE/PUPUr1vGUbFKRJXDRN8ZSn+R8Gqwb2bjiaMYaC7aobEIBFphjCDarg0Uwh8iAEjiykU8o8rsAgjGbGTMiLCibT6MDiAHznctgKJOJIqseZx5qRiURaYJ7WkpmxKykSUlJhVgZJqSwBFWKrOQRVRWLEgELYEMyGEOhbBRhlMhCJCSFhcBQFUswC5GgAEIgYxUSYnBSEAWH0M2IYIAyDDBmg5hQJ0SICrEFYgZUASIYN5k5DATOAR4LoLoGmBGQEENhIgClGoEJKsE6TBAKxilaqmbwRMUS3ckVLuLMaGPNlojw5HyGFfTMVs8Redm2fvgN/FAscXS5Amwe4+D4NQBD2IHhzx38Oef154X+i7gnB+jadTDKI54a1PLdB0ir6+0OOln87JMTGmMMVImsLsliQUgu+kuJVFjNJkpFt0sFB4IoGYib9sd+Vs1UQgGYLSEByQOTDaDneT0RktHlCMCNZTXvdm9KnJ83eE2AkCYhBKUyKmmSuow6IU2sJKQkTCElCgmY+egpJUkUUq0klMdR1eQjCJBfs7v9RimoVEkkmQqCRhOjGDxQy2RCUIij1gt7WJPHyzxFpKGjsxyMmJDg169KUdUhfFWCBGrUqgUlJCUTpVkSQlSyeEndWLb3ScsuLds629US2VWfNjc3gS1/L51cEO6PcO/onWeYxHHp7xMHWgSO9vaRg6ce92qQZ3zwEI8fA9ZboQ0A1qnIpjXpRpeWig4BQDcKTWohC5HMmGdlBQuRwIEqmTnzsC6cXQTIXeaUEhADXDCEbNirpaZaNAN3BPMwA5OA2W8YTFGpSU2S1G8s8kREUZIUSJIm/3eWpKMQCiQcaiGtpVOrskI4OnOwVtIhVRaSJLV0AgnVUVlJUmYooZCSJimKoMlIKIgIkwiZJjGNSi1skDKDK0KLhAs8OV/0OTtDEhDd9QcFhxAKICmbcSCriRGUwJEsRCp7kSwUVNaRrHbmuapTxoSAc1z4PZ6OzS6nJOeTZ7lj621qO7nCAeSFerKPcdKLGCjD5n7s/l4bfPfg21u3gDcAvH4Tm8srfmy3T2f5GJt1CFiCFYlQC3Xz38skVFTEvmtmRIHNglKqAcdNrGUUy265VUbNgDliNmmm9zW5PcoGLazLbNELOJWSqc/bghAnERWhwpRDKaIkCGxU+9ytkqJ2Qu3zKLSSWklqFJo6MVEkqSvVOpJQ7EoSklprodgRIlXuSKKQGSxk6VTEPM6cXMqRx9NIPTZGyTQyWdFkOaob7YqgkYt5QJnd+Gs3VcqYFuChGjGKURiokVisNe4BVJ61ixlmsBgJsaD13jIDwOWlv/f07Cms26cnHzXV3DfxHoA2a/FHfppx/9jLgBZKgV60PsZATQ7I3RekOI621u2t/Fz7Y+I8f+oUwBrWYGXXb3ydqIcl/74YyUSoisFQZgIpWyFKQIFYFO7uAEg5C4uYLMYIyiI9NYSFqVRs0sxWjaZESUVMKxWhZJrIPJKO4DaLQhZnsZImoUhSSy0stXDsSK0khMKljlbSzdKGChIWf1RKwt2QqDJN4ozGSkJlM2XRVWJSEhIxsiQFghLVCpiKwVUeTAHW2GQ15vibpDpH+U2R8hXzPIbW0KCFEwBAjWLIm06UIEJFEEISshhplpIBwLSuMnNeAqvAWqfn33Ht1far3gSAdx9genRK9+7dA3Y2bR/AcOCeWJOT/qkM1PY5XPibd2g5wODolN4FkE7H9Goj+l59FetwFQYAViWaFJVZnchiQdNp/pIkfnzpF2uiVAOAKrmv7N5UasIQSgYExABPwWhmx3NOZG/qtLIq68QIkMewhPPNCpCijEqIWqdKk5IQRaXKtENRayWhSn0ORUFCXcdQ6sw0YSapkkpoNtFeJKmkkkCSqCCptZI6Vj5+iVSZNLFCUmZQErap5UnLeXCcNAyOpIKkVKtPKZRa22sztvYaF6L5EQBSQotR5RVbE6D42M3txOj3pCjJ6kRYXgae1GSdik5xCmQJpOfjj0uZfdc8o50dwt0hvSwe9nEV1iTiLCaQAZiPo/f1ZLVnjwHoeEI2rYhmPhGYymhLACaYANMJ0AXQAcoOAJRzGVsUKFCj2YHEZCk1hALczvEksKJJas+GJhm5rWMQlzRqlYk0NlEyaSf9pUCSYhJWkk4ZlQNJim4HcYTUWkkiSSwkVXWZCKqVVEJFMCZtJ+IEYZ9R0S2VhaQL0y4VyrEjSbIKKwvtBJJS2Oea1nPmAZlSDtKKeHoIMcSzIkMbyPWUbajkGB2UnjGu0dInP9WGoVyMN+QFAHQBi0JUeB/u/sztIABYB/DqtVehYzdD0pkzUXdr3W4DwMALEdugKhZzbj6JgfIxRjSXQvvA7cPD9tPxvG84OsLmkv84dUtrJFATPAWAaYzW2EDVrPl0CQpstYolmaO/xoHaZCz3bjwwmTP+KKuBnH+ursagkFqpE8XtCREk0xLBGaW5iUpCQkJaZzvG1RPBlKMDcZWwLNErWnFMRKVWXKfTBOHSH1NSnZUkrDOppJKqqlM9M+WiFipI6gkJq0ujiVY1ZSZNoRRKphzyjPdAghiULEqTUtukzELJSEyBgNjQgsmIC2eiTuG51hFZQsdM0ZZ/UJYL9zIpURHs6uoKVicCgBWsoGEh7c0Im8eQywk9XOsb3gKm657uumhIe4rki5PrX56RuKDv9vaAe9vbz+6CrS3w1YWdTMZ2mv90sfD2UrFivV7XJRAAxGTAfHuURYECQMqqyY1fR4yBCE/KSkqapU0MWjQjslXN2+2ICYJ7V6SJisw4ME0zkg6icnQjlyO5pKgKraVyu2bhQTTW2eRSGaqcpjJNLMslC6dlIeoqp6nwxJFdolKX4rJxQUJTVSZT6qrWiiyNVDnUUuaheSmQpNoNfEdGa7/WThSQXxsxCcXGRkrtpmk2CcyvO8GUKOQQjCmJGFCAumxVEKMQ5vcpsU0mU/TyhraiQxe4gM+PP3X85XgTAHDzbK7G7h29Y7v3B7SP+ZqXen46A/mxd+8+MxcMCxIorfqP6XhKG70+vdJZIpwDKwD6Hh/HBFfAZApMAUrByhSJJBkCGwJbXVdt/Im8XMa9q5LN8ZugMLKkbKhyQryyUUoqgaRQtpmSJNTaGKdkydVFGV19qXtYrCQ1XB1xzEYymRvKVCoXJJx8cBuRT8Dp19EmiYSvLjQkFkZXJ/l9LqfylFKaCUlVkvB4KixVVpvqExfrIo+srKWRhklJkiWhAH+kWoVJPB9bLIl5NmOWrimHXoAIQdCk4jSqk3ugABDYahFDDZQo2z1K0RmpB2CSjWiqZ7YCAGdnsE6fuNcx4BhhuWd4/SY6J8s2Ol03bL3dzskdDoceV3+2o/0nMhANn/PA9gcDun37dvs6rud+xJubwMkJaHY1d0HLaNRa/YB7AjNUkoxCz0gmVmfGqVWsCGwU2EjFQFE9EGpOrOwKS+EJ7+4qB435mILJiuS4Ty+wJURNsRTWJI5KFkq1aVVNnXmEhQuS2azU2VR1WpBwmjpToKvLEA2JZKUTLXZYVsvCwnJPw/hMYz2WFRJ1phLtkyonFqKOUq+jQXrC+UHk7n8iSZ3StFaSkkypzIxOUQtmAwUtVIzYc5DcFnIJIwveZJM54EFfB0fBYgTTWsU3pCSjkKySZJBkmM6AKTBNwZYAYMm94ovsyjc2a3tPz09tlitYd4/esV0cAPvOQE0v8BfwyotV2J07d8yGQ+/seX9gezsju3f0S9bdWjfgQXvcawCciwHq+OAUK1zXUh0NKbgNNAMoslUxGSJbCYAoahJ2ZkEeaa1iyCOuU7YNAM/3FCYRhT+CiJAmMhHuOhYzTZqITKmutHHL61hLHUnqSMLCQt1SZzNVLkimiWQpRZuWJJMrUV7paUDSANEPE8vlpejl5amO6yDjzivCq0s6huhqOTWmrjKJTnChnKZSJZLpdKxMqoSJsrikIiq0npJ0SJUySEnqKnaWHP0W0oTkYCg1VR75moEskRhCKl6GlHwTEZnWzeYLYmVgQ5ZKFNmADigm6/Yyw9SVAX2gLAxra3MG6HeNV/v2Xn492lq3iwUJ9Nz6dBXW5CSTWTs8ZR/A7a23HR/ALT/wCHhytTSXPN2JYXUFVPtQXUzy34tgs26hiMEwAyj0rK5mWkuyIogRRS204+qpcJebkmlBUSkUeX58UEq1u8AZpCuELAUI1aZ1o8LUjdQOaZvewEVWWYUbwFxCGKr9pa6GkmW5jra63NPAqry6pIGS8rlo6Abhtb7SbGJMopfnokxJY/2KXHVZAiXlGkL1zIhUeS3bPo1KnJmyVO7pZanjw4RVSZOgARwpKsG0CJBkIpJHe0t+UHIPzjdW0CZSnxAVygYRZ5wQjCQZofABxqRKEgwTgIpouHIVRt3CaDbx+/bREzx+/BjAQ3xuITS18va2YXQwR6PvvhyJfmE+UJvYPldljK1Dug3gbNZhAJimyLU8YR1PKXXXg00rWl4NfHZ+xb2iHxRT7sRuMGOWoME6zDb1UZYzmVBMCFZ2uLLERQhkRlw0cTEAxmEeG2tOrDD+/d7bAAAZQklEQVRFDSCwJRFDGZWmYlRGpXqW1UOhjb3TkWB1JGEynZEqUUdposp9VaqjXdVXQlVhr3RKO8MZaFYadUujpY7hxCN83OuYTmYt4g40YZuKbFbT8vI6i018gF5Rko2n3Ol5HpQasVnNUxCXluFnIy5Q+3NWKlByZYlNlQoO89COBgJSW2PW2jwqRkXU2gO4WiobBR8BXkcSokJZKmmmGxKpzhILQzV2WC5ZlM9FeaPWcOIpHUWYapuZePht291esc2dTWvHYTZjErKMWbwnLzSimzGPuDM0OJxtONq2ewCaAWbAQ4TxU3ttcxOB0nyQa1kY1TOjItoMqki1MZmSJIMEq8i0LDugwFYGtpKiQsSo9hAENSotVUohZdc3SYvyYkHa1JWmjiTSJFQWWseO2z6kygVLyzxCQiRKk0tlXChfiQZSbZjngkRDn4RRazh5pCGIhs01LfosT16JErDmGXyUXEKFFaVZadR51ZhFwzhpSCRUV0bU1dlEdCZTCSULFz3pCQtR4dHiBkYIJIlUa1TavG5CI6LNawgpm4dg3OZJ3dI9OkQtKUvswNaoTKr8XyZTYIppXflY8DIaVVNbBYCswRZb8b+38dhGAG7fvo2D5zC/NjX6syDR7UcIbd9DANid5+0DyIb01haeXHk4g7qlXcxKW0U2pMtoKCubFpVRCjabqnK31K6weWS70FRl4E8hVEZ3d5Uy5B81hdKDnvD3oGKJNKUcGWeFdCrTpCT1tBbWSuqChIueILFVnpcn7pqzhJKEqasBouP6Uhiil/1zoenYTh52LKDSsLmm/J0LK46ThnHPMD61Yispf+fCwnGtYWtDeTI2Rq1MohezicXOKxKyAR5KFi5ZGB2dno6VZ1dKUGWppdLKQyYoNGklHfFIP9WZDpRHnWcbMOXAL8rozyNJB9kYz2q7xiw/z+oxu/GUghGVulTE1gOjbmHcLY1mpfGkY5vYRFjtWVzv2y3cwmBn0N7f/X3MJU/mgs8GJOZF5kBiOzgFuz4CJq+t03XC0cLxS2Oj7sQuqolxNbX+GNkeCoYi2hKVistLq2DaFTbqqHKsW6LUmoQyARvCUZ3DD5ocy8k2A2vtwCAcCSaocszpnNn2aJDjZjgtkyjV0QJELykpdUuj7th40rGwuabXN2vd6nctjJ9asT1VXj21Q1T65nnfeHxqN9/qWVjuWZicW/lKlIDKJdKM5GJDNFBfxxClemZMXZeCvY4SlVqJg5W92JVuV7XOjERUaCck4zIkjqWQkpSthM05GRkM5eDSt06mVKs2kEGHCq0r1UqSUUpuj81MpzTWGVSn5NfO1dQAgGcdo+nYAKCAj4jC+1mztOkcB8AesPerv+rmxFzyfPaU1rYyYzj0iGyuyGiGpuy+0ef33geqrZLro1MWlGzdPq1YYLExn2HCSxZZl1Z40S6Y1EJl8NHeZSjIrGaTSBbE84bKElbV7PhqhQpAmXdVLWxlCVRTd1mbXYeYjKlUmqrbOikYQZSK6AQl0auaZBUAVYWNu0FoaWz8sGNPNmvdzpP++OzU4r/at3ixYgBw8+SCHn702PDWLTzAA7yZZ78CgPbX28mL2puRdfqkJ5e8bJGtqkn60e2iOpHGbkAt1Okyq1WMVJCCPF+8XLh+jgRUMIk5/1qooQNKABKslmRlZKMQrJJkJK6+OjFYLSSUklUlCVIwLr0qg69Em43DtKyBktKk65J1fGoP46XeehdY+7HX9R6em7GRZ4t5lcPHwcSXSSBqmoYD8OTq+94HaDcnHB187bGl1XXaAsA/8oZxr2On07Fxt+OWPnp6RUmproz6bhcQqfaKYJVUwsJSVY7e1lCtKzeAqTalSNK44BTdu6FatUOm9bSWbiDpxu48HDErlVDqLH8/LXWU+l2dpImERBKop9Sd2CUlHXeD0NQlzxOtdXvVZ4zyat9urfXtva89NsB35AHG2rm27NWbJ8sWNx57Red53/gbp1ZgXctXooSTSsNTvznjHsvl6tLcLiqicWIhdNwukp6HTiIJSyU1h1RL5eki01rqulCqVevasaQm9FJPc2gmI+r1xFF1Kki6KLWemVJKxmUu6SlZlstoi8zTINBPJhd2jKRb2EY8d/X14NqyD6g7esc8BWwPezs7TT+il0qgl9tAAKwJqO6MaIihYmdkBwAGGADXli2un9rj8amFybnR5MJwdWFnsyu7mE1srVvYarnu4vyssgmpUqqsYSLqljkOZUpFDmxGklqcQE3AkoUkUUypE1OilDhC6sKPa9BkFnfRiUpPIk9T4SvVFeoqV1Nj9rnqoX9NTilp2FzT8pUo28s9OzyqFBuPrRuTPsyjkjon12y0tW44/HbOxPTpNw86r2vcWPEhwmt949VT43HfuN81Wu5aoKQ0K22DVnTc8XnubhdNZJqmMk0kUxorl8483Eb4YyJ1GjCllDoxcSDhiavtJCTd0GymbOt0HVVnMq1IdZZDMtOUXfi6ssnVhQLARddzjWhW2unJim5ubmIbwOPxmV/fuw+ArXU7WOjWMbh/3wa5Nswlyvdb2oxc3XznjtcI7eyRJ5btYrR16F/2vgdWed2JuLl002ipY0zLejGbGFVTC6Q6gWi/iDZDxz2UqSrP5uEEf7DUlSTqqqdWKEldh0SFp1w0oQIWlmpcOeYydqLNCpLZRHTaoMpXorHDErssl1hyDCesKE/GtjnpGn/nwnh8aiUqvbWx4juwM9POtWXD4bqNtg4NR+8Ybt/GCAN1ZHagtwE8+MOZdk4eWdxYsW68oQ/Pf9fCuGdFdLUQKOnZ7MqoW1rosQQs6dU46XLfacDJz5WoVC5JlmaiBN9MLCyE0j0prYT6Oeir5DlL5HTgouc4U0EyG7u3t9TrKC2JcslCdWVu800tQPSVbDifUtLNpa4V0b3mdr7qWw2253PlF1pwfmJZ8ycx0HMtjQHcH9jx5oBab2xr3d7D2MdLP3yIx+Ondty/sM1+z2g69gI2SkrV1FaXe27IddxDmfbrRDlpa4lKDVJJRTG9EtnCLKdWRA+AspAE+N9YSLgg6RckQSrhgmRakHs9JctKuSyTRBKWe3rJohez0kI3eEXmZGw0ubACSR9vO+4RNx5b5+SRPejMdPfoHeserhu23zEcvePT/o7eMYyGtjnatAMAK0fbtru9baOtdXvQmSkA3Nr4cStRKY9Pbat/YYFrjStRnlYsT2dju6RTXS0LC+Okk0TuCebzrRLJrFgSLmcShN0ZkEoCJAUsz6+5ocXY8R2aTnzjzVSX0NHpdKxVmspyI+2vRAP1NKCnl9RXnk3sKWp9FQD3L1xqrp66qr5xrb2WZu3PR0Tl/k3fZ0405v2ckCt8AAC7XxrqAeCcCqBx+uJ6326u3rLt1Z6dfHBp3OsYTUvj7mt2SUmZ/YKYRZfLaP3aMaIZVB9RSlMqlUj1YiaJM0P1pZJQslTCMpupVqUHLolUp1RqRTGdpqn0SXWaJs44pMpdD0O8MiuNN0QDJY3TKIFrLbCuvHpqb5737b2PHlvnZNlG+XoOsIt725lxcjYeRpuGvT0MBvuGnZF3r8ABkO3Azskje5ClUTzv20mYahj3jL9zYZvLXQOtqG+kvo47JK90Wa7GSSdpLHx1oUSq0yYUQqoVx8SlS1Umz0sK+VFJJaHo+fMyS90SUhUkjI72U7TJ1YVOyrGsLvfUpb8j6Wezsb3a69gTrjWs9uwhHiKe9y1+9Nhw34uacfSOLaZwWGaehRFEL1yfKJ/MuxTn0mZfezsj2s+t7W5vHdJZLjIEmsaa61QfRbbelHQyI0XBq6/1SKeedH9e1rRkka3qEPqewWh1SYiJrE5khVAPHsCZNCdZV0YZz+gBmJSVUe1YE9UzozoaV1Pj1SXlWWkX9EQBINCKBvZsvzDuGa+e2sN4Q2+9+wAPri0bcvLUytE7djA6sOEANtrZo/19hy/aFifNyq1x95Cbdh96lcpg+5RmGzconVwQ3gCuVSU/PJ+QLk3JLldILLJNK8IasGyB9fyKsbwCsQkvFR26rPzarwAsxZw9CM8kpKIyq0uiurJuUdIMOpcWyelCZbSrpzNbLh2moG5hlySe8jEbW6AVpeWu8dVFizrHjRXrnDxyW69loFx9szAi0+7csZfZP5/GQITG6s5jLIfIrX2B1qUfHDnxgAeYpmV+COC6vME6vqKGiazbJ7VLXltbgzwds1U1NQQEgDGAngXHHOqSgKt8CkugIkf2rwAsZWypnBl1onFV2EU1beM7Tdku4CEImlyYl+/2DXiIh9+51MG1ZcPOAN3TQ6/CzE1CgQN9NpF3oRe2B5abRCvC/j4w2KXd5pNtv4BLSife3hh46K5+igwAcnzGttUnu2rCID1axhUDXm5jRaLxGOgh8NUSckZ5vol1IiwtAVdXbVyrvwRMSJQ60XAJcDk1qjaMqqldQvSVNeA8rOgGgLOTR/OQxXrf8D7Qx1i7W+s+Ufto2/Z2RrZ/32fLDzF3oF7kui+uT27v4qkd7TFD5AqN+z75Zfdo27pb6+7m4ha68Ya+ed63sHpu3L8wmlxY2FzTsOku7vmH7krz6pJSNbWrmuRqnHS5jhYSySp5gJPR1VAuS6gn4pHvroYNkpBIYoflqiYJl6LMomtVYRvkoQXeWFHuOTDYqKzDo0rjeR6JfW3ZAKD7m4d272jb2l5HO5vW9Afc29lpN9XwTs5IyBOPhwDwl/YNg6HtZbvoAMDu9juGrXXrnCxbH2O9tb5mwE3wuG9FTMpXF3aC7OqHWpmWlSEac5Q/wFX86nJPQ0mykkgmiYRJdHIlOq1JQoOkX4kyRKcZnuCTwrgsjHlJA6mOu0F4Y0UvK5ZwInoxeyrOPK9qXHeMK+awxb0WGD7wwXTP9Uhc7JLwsvXJKmyhzQuAeZ/EnRE1YnwXwEEGF71ZEfDg5IJuZnUm5xPSpRVqmi8AHoxcnfboDGdYyX2ELrqFrUxrsrJLF9XUVqqaLjBPwaRymkMmDsef54gyzTz42RjJ3O9aWM3A4Lhv8fzU3sSbOMBIBwBGh+uG7W3DaGSeKbeH4f6+3YE3Xx7evUt37gyfzd+knKQAe6ZX5N7eHvax71/z0+u+0e49q9LS6jpd74/p3hGA/oQwntIGSqdDt+/SaFrR2toadFbR+SqwMq2pkdK4vACWV3BxeYGV5l8AF2Vha93CcA5wZ2LnORAMAA1QyP0Lc9Xdt7juRnO/P1YAGP3mtw24DWy759Uk0e8PBubSJ89X/YTuZJ/KQFiMzDf5aAuivLUFAAx2dhn3R5h9dEl46xYenPwB3Vzd9H7R/Qk9fgxs/vAKyfGZE3pjA3Y1ozkz+VoDoLOK+LXSzs7O0Eb+4CkjT2djWwcArIOmYzfYl7t2fHVh13EdYfXceOzE6sYbrb2w2MptczRyI3mQW5gg7707bezn2ew7y3/LecHDu3dpOBwCwyH2FhtPuDwCMOLBDjB7dIMevPsAn/vhH+UP8BANLXTJO4TZpaPYJ9+raH19Hdap6OkZ8EO57OYMZ1jLuTs6rehiVtpKp6KL2cSANdBsYlhzO4eWbhhOThBQuQq/7FpYdrsvnnsCYNx4bJ0bOesQaN123N83jPZoOBjYMHfjWGhx+INLoPaYRQIuGNTtvIV9YHcwoIOtQ8K9e7j1Y3+GgaZz65j0fJ2wBcj5Kun4irAJ2KUT8clkRus5TQIAbFrR0+7Y1uFsstZZorPZlZ1Ox7YBABsbwMkJTjbX9E9dTulpYySv9uzwG6d283XHpuLGinVuXLPR/ZEDgfcAZC9rv2ld0vzmnbaB0vMU+XgaZ/OphUAzMAR2ct/snU27fbrO9wA09uGDk0fmzbiWGbiJh3iI69JlHU/zd2xi0V4EgNPp2Na73qi0+ZWns7G9Mu1TI2loOjZsbIAnY6PlrkvdK5c6AMCrLoEBYNFovg3gXsM8wNxobitx6Pnt89L1mRioFWNe3T/3yhZjZAAaz2y6vk2j+yPcasqg0eRR34Seez9FXPec6oZ4zY89AbDRmxFPOvYEwKvPn8xy1zYupxSXu/bk6sIeh6lun69Ts9Pixoohe1kDeJnKvaOssvaAYTYUAWA+S92NxbztFpnmeeLN92Rz2GL67wI9dpFV+717GGz/CDWSufFYq62ScQQcAhgg8oc4xvHlmb26ebO1ORevXycz4l7HtNcnnngw9MnkwjaxCeAY3HcGCuOe8dmpxbVMCyym4ACbGOjBTwI4njPPEGgN53Y60qcYz8361FEHCzqQWgoOh954aDCwPewBGTsBDrBytG2j00Mb7AzQueZGZYOTxPNT49VTK7an+jhMtdha12IraeC6fVxnT3Si5a5dZ0+faN4rJ1GK46TfnBzLaUxahKne/AAoUWk33tA+xvrg5JE9+LHXdRfHuomB3suYVVOfnXPEnYXy0KSmfdtCY217KfEMeXxGVut3hq0abKVaNrAdL7qN0da6Pfix17Vz41pLj7KstESlN1dP7Wz1WAqs6/bymxa41uuTrrW0yI+TPkvY2vBcJa71yXGt1/seEC2wrkWYahGmWr5Racs873r68Whr3UZb67aJgR7gYM48ueyiOf/5rLXPIld8fSoDUe5Olb/Z5kQk4M4d76W3UP+xuTOy3aNtG92Hjg6/7ajtyaMWte7GS43rfXtzvW+HR47gFiEzVFhgrOMPNIyfWtGV9j1e7RuvOmhZHlWOZ6x5Jt2DZpdtrdtuwzQ4AHZGtjdyI3F4f9C64+1NJyzaPC9nHOQrp2eaOALwHPJW/N/P468aTGV728/nHjA6PbTR4betc/LI/pWLFYsfeXD24fmxo9mrfSvCVE/xgYZxz/zx1B6PnxrCVMO3z60zO5aw2jPIVIvwqvL41Mo3Kn14fuyb9MJjdR6a+dP24A9nehvA3m+euj24s2mtyhrMpc+C/Zedhk8VPu3hn7rm3hjmTARQ4962UwuRm3EC2B042HiQU2GnTVtgAP/2xg16Nz9v5ze8kV+ferpEW/nxPvDmG2/ivfffA+CG4IOTZWs8vs6Na4b7I8wBsYxpLF7AAjCG7GG07PIZdf0LFmXaoOkpP8RdGt6dM+doZ0T7+06LA+xiFwe4yGptuv0jBAAjeLfbRlo0Kv+9jRV7MwOTeB/wq38Pb77xJvC+0+u9i8d2C64WF9XV6HDdBtunNNpaN8+e8ADUXrZ59n0jZXhiaC0h5p3IPjM9PrOsar7YDaEFE4HI0GBFoxFhMLBnbCPneLq9dUj3jt6xBnwcba3bIDNVwwyA9y7GW8At3PI+xm/dwoOTR9Zmy90fATvZDb19G61BCPd/Dhbg+L19d0vba7hzx1q+mds8n5lYn0SXxY641AymQd5Qg33Dzp5vrPvOTE2r5KZYapoHoADeJWORHreaYCeAB3jgtMk0Ge3vYzDYwejw23b79u15zl9mnGcqTHOrnraBJgA853V9mtv+/Prsym7hM82P2MJcGX8nG5XDIbC3l3OJ9g3YZexs2u79gZeLbM2LFG8DuHfvHgY//RdolOMyA/jOBABkRmtQ09vwxEgPQWzbwU/m455vDpnd7CGeNRAz0zff/gMzzgvWfFc1Kn8xBDQa0f7gmDDaNAzmLv8ucu/tw7ep5aZ794Dbt/1CtzNjHX7bBts/4lI8S1vfPKd0e2vd7t2DYzpY2Eg+v1Yx8nsxHAzsznBoNByS3Rka3R0ShkPFoqP0A170D7I+jhI849ourAYzAlqP7fbW2y6Rcv7JLg4yIPmO7WIXF1tbdO/oqCUIkAmd1+5P7uLg2FMPBvf3bTjac+mXWcYyoXDnjjUSZ2H9s2ScxfUMTVqPtVEX3unWMBrRcLBvw8b1X1h7o03bHwxymOQAB4dv0+28a+5tZ3ptHZIHtA+e/fUs7fdGWVXBmWa4cEgjhZvT/X48rpdf8A+4LA//WBB9H0etF1dGb4F9T9rOnlHTBW2RwfI+wp6XlzTg5TPew177mYE1IYj9vT11CTMXBNm3cuf7B9xpn2U9Zys2aXnzAxZtRTxvMw5oc2dki3QBXOVtjjZtP9uU7hhs2h7g4YeGCHmHDgeLmymvxsC/e5de4qq3Z/v9rj8SAzWrtY8yE1ljky1gJEM8H6pc+NvOiFwvDx2QywRpbZidEQ3v73uk3CtlaX/RMIanHxDQZiDM3cV8of/fqK0XrecGxuTVqA3AJfWdOzZcYChg6EP8BvvZM/L/L9IDwIJDMEQT+Gxtzxes4R1giKEN7w7J83vwcaT9j0CTfyYM1HxP4/ot7sK87ed31fWuNc+HAIZ37tiQyMMDeG6HLvxI+9oNP7Re4POXQ8+cyx9ph/1R1uI47dZbWwiJ4O5d94Ia1b8Y9X/eu33B9w+RaXf3Lg3v+MxTGmb6zq+3Hdr3HL7zz50en3U9P8Cecl5RfphH+PPzjz2QjxsO2eZoyzPH2MIDcLMYcyaef+aPx5rT4ZlzzjTBnC5YuF5rrnc4ZLT0cro0x9oztPPXLW2fvQ/t808bovvHeX3soj72yESyBYLAPsYYwLM3o0k5oYXf+eO+Ps4sC8/b18/Qobnx9uxnFpjwj+Hm+ee2nrnwdrfgmd3yLxxR8PyG8dlRL2KQfxFp8/H1nJQB/oQwzzDQ/69n8ifrT9afrD9Zf+zW/wvBv0ghncWyVwAAAABJRU5ErkJggg==","e":1},{"id":"image_1","w":174,"h":235,"u":"","p":"data:image/png;base64,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","e":1},{"id":"comp_0","nm":"左滑手势","fr":24,"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"hand.png","cl":"png","refId":"image_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.328],"y":[0]},"t":0,"s":[28]},{"t":20,"s":[0]}],"ix":10},"p":{"a":0,"k":[80,148.775,0],"ix":2,"l":2},"a":{"a":0,"k":[87,235,0],"ix":1,"l":2},"s":{"a":0,"k":[33,33,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[96,80,0],"to":[-5.417,0.167,0],"ti":[5.417,-0.167,0]},{"t":20,"s":[63.5,81,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-39.687,-13.554,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[26,26],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":6,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0.54,1,1,1,0.77,1,1,1,1,1,1,1,0.54,0.6,0.77,0.7,1,0.8],"ix":8}},"s":{"a":0,"k":[0,0],"ix":4},"e":{"a":0,"k":[0,16],"ix":5},"t":2,"h":{"a":0,"k":0,"ix":6},"a":{"a":0,"k":0,"ix":7},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":13},"bm":0,"nm":"Gradient Stroke 1","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-39.687,-13.554],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[141.5,82,0],"to":[-5.667,0.167,0],"ti":[5.667,-0.167,0]},{"t":20,"s":[107.5,83,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-12.5,-1.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-12.5,-45.5],[-12.5,42.5]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":8,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,1,1,1,1,1,1,1,0,0.3,0.5,0.15,1,0],"ix":8}},"s":{"a":0,"k":[0,42],"ix":4},"e":{"a":0,"k":[0,-20],"ix":5},"t":1,"lc":2,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":13},"bm":0,"nm":"Gradient Stroke 3","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"dislike.png","cl":"png","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":16,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.747,"y":1},"o":{"x":0.502,"y":0},"t":0,"s":[143.297,137.422,0],"to":[0,-7,0],"ti":[0,7,0]},{"t":8,"s":[143.297,95.422,0]}],"ix":2,"l":2},"a":{"a":0,"k":[72,79,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.506,0.506,0.667],"y":[1,1,1]},"o":{"x":[0.974,0.974,0.333],"y":[0,0,0]},"t":4,"s":[33,33,100]},{"i":{"x":[0.057,0.057,0.667],"y":[1,1,1]},"o":{"x":[0.535,0.535,0.333],"y":[0,0,0]},"t":16,"s":[40,40,100]},{"t":24,"s":[33,33,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":16,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.787,"y":1},"o":{"x":0.482,"y":0},"t":0,"s":[143.297,137.422,0],"to":[0,-8,0],"ti":[0,8,0]},{"t":8,"s":[143.297,89.422,0]}],"ix":2,"l":2},"a":{"a":0,"k":[12.19,-11.31,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[80,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.396,0.914,0.835,0.5,0.198,0.796,0.712,1,0,0.678,0.588],"ix":9}},"s":{"a":0,"k":[-40,0],"ix":5},"e":{"a":0,"k":[40,0],"ix":6},"t":1,"nm":"Gradient Fill 2","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.19,-11.31],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"左滑手势","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150.5,169,0],"ix":2,"l":2},"a":{"a":0,"k":[80,80,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":160,"h":160,"ip":0,"op":48,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/Meet/meet_right_swipe_like.json b/crush/Crush/Src/Resource/LottieResources/Meet/meet_right_swipe_like.json deleted file mode 100644 index fdcd1c5..0000000 --- a/crush/Crush/Src/Resource/LottieResources/Meet/meet_right_swipe_like.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.9.6","fr":24,"ip":0,"op":48,"w":300,"h":300,"nm":"右滑喜欢","ddd":0,"assets":[{"id":"image_0","w":180,"h":180,"u":"","p":"data:image/png;base64,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","e":1},{"id":"image_1","w":174,"h":235,"u":"","p":"data:image/png;base64,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","e":1},{"id":"comp_0","nm":"右滑手势","fr":24,"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"hand.png","cl":"png","refId":"image_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.328],"y":[0]},"t":0,"s":[-8]},{"t":20,"s":[30]}],"ix":10},"p":{"a":0,"k":[70,148.775,0],"ix":2,"l":2},"a":{"a":0,"k":[87,235,0],"ix":1,"l":2},"s":{"a":0,"k":[33,33,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[43,80.75,0],"to":[7.552,-0.125,0],"ti":[-7.552,0.125,0]},{"t":20,"s":[88.313,80,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-39.687,-13.554,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[26,26],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":6,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0.54,1,1,1,0.77,1,1,1,1,1,1,1,0.54,0.6,0.77,0.7,1,0.8],"ix":8}},"s":{"a":0,"k":[0,0],"ix":4},"e":{"a":0,"k":[0,16],"ix":5},"t":2,"h":{"a":0,"k":0,"ix":6},"a":{"a":0,"k":0,"ix":7},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":13},"bm":0,"nm":"Gradient Stroke 1","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-39.687,-13.554],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[6,82,0],"to":[7.917,0,0],"ti":[-7.917,0,0]},{"t":20,"s":[53.5,82,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-12.5,-9,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-12.5,-45.5],[-12.5,42.5]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":8,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,1,1,1,1,1,1,1,0.002,0.3,0.501,0.15,1,0],"ix":8}},"s":{"a":0,"k":[0,-28],"ix":4},"e":{"a":1,"k":[{"i":{"x":0.004,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,-3],"to":[0,3.833],"ti":[0,-3.833]},{"t":20,"s":[0,20]}],"ix":5},"t":1,"lc":2,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":13},"bm":0,"nm":"Gradient Stroke 2","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"heart.png","cl":"png","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":16,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.747,"y":1},"o":{"x":0.502,"y":0},"t":0,"s":[143.297,137.422,0],"to":[0,-8,0],"ti":[0,8,0]},{"t":8,"s":[143.297,89.422,0]}],"ix":2,"l":2},"a":{"a":0,"k":[90,90,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.506,0.506,0.667],"y":[1,1,1]},"o":{"x":[0.974,0.974,0.333],"y":[0,0,0]},"t":4,"s":[33,33,100]},{"i":{"x":[0.057,0.057,0.667],"y":[1,1,1]},"o":{"x":[0.535,0.535,0.333],"y":[0,0,0]},"t":16,"s":[40,40,100]},{"t":24,"s":[33,33,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":16,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.787,"y":1},"o":{"x":0.482,"y":0},"t":0,"s":[143.297,137.422,0],"to":[0,-8,0],"ti":[0,8,0]},{"t":8,"s":[143.297,89.422,0]}],"ix":2,"l":2},"a":{"a":0,"k":[12.19,-11.31,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[80,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.761,0.255,0.902,0.5,0.855,0.324,0.773,1,0.949,0.392,0.643],"ix":9}},"s":{"a":0,"k":[-40,0],"ix":5},"e":{"a":0,"k":[40,0],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[12.19,-11.31],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"右滑手势","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150.5,169,0],"ix":2,"l":2},"a":{"a":0,"k":[80,80,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":160,"h":160,"ip":0,"op":48,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/Meet/meet_up_swipe_see_more.json b/crush/Crush/Src/Resource/LottieResources/Meet/meet_up_swipe_see_more.json deleted file mode 100644 index f7c4e4b..0000000 --- a/crush/Crush/Src/Resource/LottieResources/Meet/meet_up_swipe_see_more.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.9.6","fr":24,"ip":0,"op":48,"w":160,"h":160,"nm":"上滑手势","ddd":0,"assets":[{"id":"image_0","w":174,"h":235,"u":"","p":"data:image/png;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"hand.png","cl":"png","refId":"image_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.328],"y":[0]},"t":0,"s":[-90]},{"t":24,"s":[-60]}],"ix":10},"p":{"a":0,"k":[140.08,71.138,0],"ix":2,"l":2},"a":{"a":0,"k":[87,235,0],"ix":1,"l":2},"s":{"a":0,"k":[33,33,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":36,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[72.063,88.446,0],"to":[0,-6.167,0],"ti":[0,6.167,0]},{"t":24,"s":[72.063,51.446,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-39.687,-13.554,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[26,26],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":6,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0.54,1,1,1,0.77,1,1,1,1,1,1,1,0.54,0.6,0.77,0.7,1,0.8],"ix":8}},"s":{"a":0,"k":[0,0],"ix":4},"e":{"a":0,"k":[0,16],"ix":5},"t":2,"h":{"a":0,"k":0,"ix":6},"a":{"a":0,"k":0,"ix":7},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":13},"bm":0,"nm":"Gradient Stroke 1","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-39.687,-13.554],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[72.25,124.25,0],"to":[0,-5.917,0],"ti":[0,5.917,0]},{"t":24,"s":[72.25,88.75,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-12.5,-9,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-12.5,-45.5],[-12.5,42.5]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gs","o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":8,"ix":10},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,1,1,1,1,1,1,1,0.002,0.3,0.501,0.15,1,0],"ix":8}},"s":{"a":0,"k":[0,-28],"ix":4},"e":{"a":1,"k":[{"i":{"x":0.004,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,28],"to":[0,-3],"ti":[0,3]},{"t":24,"s":[0,10]}],"ix":5},"t":1,"lc":2,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":13},"bm":0,"nm":"Gradient Stroke 2","mn":"ADBE Vector Graphic - G-Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/Voice/call_ringtone.mp3 b/crush/Crush/Src/Resource/LottieResources/Voice/call_ringtone.mp3 deleted file mode 100644 index 0e83065..0000000 Binary files a/crush/Crush/Src/Resource/LottieResources/Voice/call_ringtone.mp3 and /dev/null differ diff --git a/crush/Crush/Src/Resource/LottieResources/Voice/voice_im_recording.json b/crush/Crush/Src/Resource/LottieResources/Voice/voice_im_recording.json deleted file mode 100644 index 38b0ac3..0000000 --- a/crush/Crush/Src/Resource/LottieResources/Voice/voice_im_recording.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":24,"ip":0,"op":20,"w":470,"h":80,"nm":"voice","ddd":0,"assets":[{"id":"comp_0","nm":"wave","fr":24,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"line1-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[223.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[45]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[30]},{"t":20,"s":[45]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[55]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[70]},{"t":20,"s":[55]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"line5-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[30]},{"t":20,"s":[15]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[85]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[70]},{"t":20,"s":[85]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"line4-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[175.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[30]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[10]},{"t":20,"s":[30]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[90]},{"t":20,"s":[70]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"line2-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[151,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[20]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[80]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"line5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[127.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[10]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":20,"s":[10]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[90]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[80]},{"t":20,"s":[90]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"line2-1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[103.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":20,"s":[20]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":20,"s":[80]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"line4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[80,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[20]},{"t":20,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[80]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"line3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[55,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[45]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":20,"s":[45]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[55]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":20,"s":[55]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"line2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[32,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[40]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[25]},{"t":20,"s":[40]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[75]},{"t":20,"s":[60]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"line1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[7.5,39.875,0],"ix":2,"l":2},"a":{"a":0,"k":[-181,-3.625,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-181,-33.5],[-181,26.25]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[20]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[80]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"wave","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[355,40,0],"ix":2,"l":2},"a":{"a":0,"k":[115,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":230,"h":80,"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"wave","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,40,0],"ix":2,"l":2},"a":{"a":0,"k":[115,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":230,"h":80,"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/Voice/voice_white.json b/crush/Crush/Src/Resource/LottieResources/Voice/voice_white.json deleted file mode 100644 index 17fc4b6..0000000 --- a/crush/Crush/Src/Resource/LottieResources/Voice/voice_white.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.6.10","fr":24,"ip":0,"op":16,"w":28,"h":28,"nm":"voice","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2,14,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":16,"s":[100,40,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-11.875],[0,12.125]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[0]},{"t":16,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[100]},{"t":16,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":16,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[26,14,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":16,"s":[100,60,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-11.875],[0,12.125]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[0]},{"t":16,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[100]},{"t":16,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":16,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[14,14,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-11.875],[0,12.125]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[40]},{"t":16,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[60]},{"t":16,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":16,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/generating.png b/crush/Crush/Src/Resource/LottieResources/generating.png deleted file mode 100644 index ea1e6cf..0000000 Binary files a/crush/Crush/Src/Resource/LottieResources/generating.png and /dev/null differ diff --git a/crush/Crush/Src/Resource/LottieResources/heart_beat_wave_lite.json b/crush/Crush/Src/Resource/LottieResources/heart_beat_wave_lite.json deleted file mode 100644 index b1e13ed..0000000 --- a/crush/Crush/Src/Resource/LottieResources/heart_beat_wave_lite.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.6.9","fr":24,"ip":0,"op":48,"w":124,"h":124,"nm":"合成 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Up","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[94,65,0],"to":[-15.667,0,0],"ti":[15.667,0,0]},{"t":48,"s":[0,65,0]}],"ix":2},"a":{"a":0,"k":[138,58,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0]],"o":[[0,0],[0,0],[23,0],[23,0],[23,0],[23,0],[23,0],[23,0],[0,0]],"v":[[138,58],[-138,58],[-138,-58],[-92,-48],[-46,-58],[0,-48],[46,-58],[92,-48],[138,-58]],"c":true},"ix":2},"nm":"path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"mergh path","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,0.314,0.671,0.499,1,0.239,0.502,1,1,0.165,0.333,0,0.8,0.5,0.5,1,0.2],"ix":9}},"s":{"a":0,"k":[0,-50],"ix":5},"e":{"a":0,"k":[0,20],"ix":6},"t":1,"nm":"Gradient Fill","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[138,58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"New Shape 3","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[-37,67.25,0],"to":[13.805,0,0],"ti":[-19.24,0,0]},{"t":48,"s":[54.8,67.25,0]}],"ix":2},"a":{"a":0,"k":[115,58,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0]],"o":[[0,0],[0,0],[23,0],[23,0],[23,0],[23,0],[23,0],[23,0],[0,0]],"v":[[138,58],[-138,58],[-138,-58],[-92,-48],[-46,-58],[0,-48],[46,-58],[92,-48],[138,-58]],"c":true},"ix":2},"nm":"path2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"merge","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":2,"k":{"a":0,"k":[0,1,1,1,1,0,0,0],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[100,0],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":true},{"ty":"fl","c":{"a":0,"k":[0.5,0.5,0.5,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[138,58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"3","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"G","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72,64,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[51.03,89.746,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[218.321,205.245],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"path1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.455,0,0.204,0.499,0.727,0.057,0.359,1,1,0.114,0.514],"ix":9}},"s":{"a":0,"k":[-76.201,86.642],"ix":5},"e":{"a":0,"k":[89.749,-75.465],"ix":6},"t":1,"nm":"Gradient Fill 2","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-18.063,-2.243],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[143.92,71.442],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"New Shape 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[-37,67.25,0],"to":[13.805,0,0],"ti":[-19.24,0,0]},{"t":48,"s":[54.8,67.25,0]}],"ix":2},"a":{"a":0,"k":[115,58,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0],[-23,0]],"o":[[0,0],[0,0],[23,0],[23,0],[23,0],[23,0],[23,0],[23,0],[0,0]],"v":[[138,58],[-138,58],[-138,-58],[-92,-48],[-46,-58],[0,-48],[46,-58],[92,-48],[138,-58]],"c":true},"ix":2},"nm":"path2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"merge","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.455,0,0.204,0.499,0.727,0.057,0.359,1,1,0.114,0.514],"ix":9}},"s":{"a":0,"k":[0,0],"ix":5},"e":{"a":0,"k":[100,0],"ix":6},"t":1,"nm":"Gradient Fill 3","mn":"ADBE Vector Graphic - G-Fill","hd":true},{"ty":"fl","c":{"a":0,"k":[0.5,0.5,0.5,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[138,58],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"3","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"cp":true,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/heartbeat_pink.json b/crush/Crush/Src/Resource/LottieResources/heartbeat_pink.json deleted file mode 100644 index 778ee5d..0000000 --- a/crush/Crush/Src/Resource/LottieResources/heartbeat_pink.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.9.6","fr":24,"ip":0,"op":72,"w":800,"h":800,"nm":"hearbeat 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"heart 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[10]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[30]},{"t":44,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[399.704,390.624,0],"ix":2,"l":2},"a":{"a":0,"k":[-3.094,150.026,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.378,0.378,0.667],"y":[1,1,1]},"o":{"x":[0.288,0.288,0.333],"y":[0,0,0]},"t":0,"s":[60,60,100]},{"t":44,"s":[120,120,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[28.504,0],[13.5,-13.5],[-0.809,-33.99],[-10.627,-9.374],[0,0],[-42.896,35.66],[-1.5,41],[38,1],[0,0]],"o":[[-29,0],[-13.5,13.5],[1,42],[10.627,9.374],[0,0],[33.286,-27.671],[1.499,-40.985],[-37.501,-0.987],[0,0]],"v":[[-60.75,-123.75],[-121.75,-98.25],[-145,-33.5],[-74.627,63.626],[-3.5,122.5],[77.714,52.671],[138,-41.5],[62.5,-124],[-3.5,-98.5]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,0.984,0.771,1,1,0.969,0.541,1,0,1,0.5,0.5,1,0],"ix":9}},"s":{"a":0,"k":[0,-240],"ix":5},"e":{"a":0,"k":[0,120],"ix":6},"t":1,"nm":"Gradient fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.407,150.789],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":72,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"heart 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":32,"s":[20]},{"t":68,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[399.5,400.527,0],"ix":2,"l":2},"a":{"a":0,"k":[-3.094,150.026,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.338,0.338,0.667],"y":[1,1,1]},"o":{"x":[0.269,0.269,0.333],"y":[0,0,0]},"t":0,"s":[140,140,100]},{"t":60,"s":[220,220,100]}],"ix":6,"l":2}},"ao":0,"ef":[{"ty":29,"nm":"高斯模糊","np":5,"mn":"ADBE Gaussian Blur 2","ix":1,"en":1,"ef":[{"ty":0,"nm":"模糊度","mn":"ADBE Gaussian Blur 2-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.024],"y":[1]},"o":{"x":[0.998],"y":[0]},"t":0,"s":[10]},{"t":60,"s":[40]}],"ix":1}},{"ty":7,"nm":"模糊方向","mn":"ADBE Gaussian Blur 2-0002","ix":2,"v":{"a":0,"k":1,"ix":2}},{"ty":7,"nm":"重复边缘像素","mn":"ADBE Gaussian Blur 2-0003","ix":3,"v":{"a":0,"k":1,"ix":3}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[28.504,0],[13.5,-13.5],[-0.809,-33.99],[-10.627,-9.374],[0,0],[-42.896,35.66],[-1.5,41],[38,1],[0,0]],"o":[[-29,0],[-13.5,13.5],[1,42],[10.627,9.374],[0,0],[33.286,-27.671],[1.499,-40.985],[-37.501,-0.987],[0,0]],"v":[[-60.75,-123.75],[-121.75,-98.25],[-145,-33.5],[-74.627,63.626],[-3.5,122.5],[77.714,52.671],[138,-41.5],[62.5,-124],[-3.5,-98.5]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,0.984,0.771,1,1,0.969,0.541,1,0,1,0.5,0.5,1,0],"ix":9}},"s":{"a":0,"k":[0,-240],"ix":5},"e":{"a":0,"k":[0,120],"ix":6},"t":1,"nm":"Gradient fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.407,150.789],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":72,"st":0,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/like_motion.json b/crush/Crush/Src/Resource/LottieResources/like_motion.json deleted file mode 100644 index af8ee10..0000000 --- a/crush/Crush/Src/Resource/LottieResources/like_motion.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.1","fr":24,"ip":0,"op":24,"w":144,"h":144,"nm":"48-25","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72,72,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":6,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":12,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[110,110,100]},{"t":23,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.65,0],[-1.44,-1.33],[0,0],[0,-2.1],[1.33,-1.44],[0,0],[0,0],[1.16,0],[0.78,0.7],[0,0],[0,0],[0,2.09],[-1.49,1.48],[-2.1,0],[-1.27,-0.91]],"o":[[1.99,0],[0,0],[1.49,1.48],[0,1.98],[0,0],[0,0],[-0.81,0.81],[-1.07,0],[0,0],[0,0],[-1.49,-1.48],[0,-2.1],[1.48,-1.49],[1.66,0],[1.27,-0.91]],"v":[[4.39,-10.5],[9.54,-8.5],[9.77,-8.27],[12,-2.9],[10,2.23],[9.77,2.46],[2.95,9.28],[0,10.5],[-2.77,9.45],[-2.95,9.28],[-9.77,2.46],[-12,-2.9],[-9.77,-8.27],[-4.39,-10.5],[0,-9.13]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"位移路径 1","a":{"a":0,"k":-1,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":2,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":7,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72,72,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":6,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":12,"s":[120,120,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[110,110,100]},{"t":23,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.65,0],[-1.44,-1.33],[0,0],[0,-2.1],[1.33,-1.44],[0,0],[0,0],[1.16,0],[0.78,0.7],[0,0],[0,0],[0,2.09],[-1.49,1.48],[-2.1,0],[-1.27,-0.91]],"o":[[1.99,0],[0,0],[1.49,1.48],[0,1.98],[0,0],[0,0],[-0.81,0.81],[-1.07,0],[0,0],[0,0],[-1.49,-1.48],[0,-2.1],[1.48,-1.49],[1.66,0],[1.27,-0.91]],"v":[[4.39,-10.5],[9.54,-8.5],[9.77,-8.27],[12,-2.9],[10,2.23],[9.77,2.46],[2.95,9.28],[0,10.5],[-2.77,9.45],[-2.95,9.28],[-9.77,2.46],[-12,-2.9],[-9.77,-8.27],[-4.39,-10.5],[0,-9.13]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.882352948189,0.164705887437,0.164705887437,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6,"op":24,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/loading.json b/crush/Crush/Src/Resource/LottieResources/loading.json deleted file mode 100755 index d51a29f..0000000 --- a/crush/Crush/Src/Resource/LottieResources/loading.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.1","fr":24,"ip":0,"op":192,"w":480,"h":480,"nm":"合成 1","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"预合成 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[280,280,0],"ix":2},"a":{"a":0,"k":[280,280,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":560,"h":560,"ip":144,"op":192,"st":144,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"预合成 1","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":0,"k":[280,280,0],"ix":2},"a":{"a":0,"k":[280,280,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":560,"h":560,"ip":48,"op":96,"st":48,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"预合成 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[280,280,0],"ix":2},"a":{"a":0,"k":[280,280,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":560,"h":560,"ip":96,"op":144,"st":96,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"预合成 1","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-360,"ix":10},"p":{"a":0,"k":[280,280,0],"ix":2},"a":{"a":0,"k":[280,280,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":560,"h":560,"ip":0,"op":48,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状结合","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.475,11.898,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.53,-0.49],[0,0],[0.01,-0.72],[0,0],[-3.98,-5.41],[-21.15,0],[-9.07,16.19],[0.11,7.13],[0,0],[0.56,0.5]],"o":[[-0.53,-0.49],[0,0],[-0.56,0.5],[0,0],[-0.12,7.12],[9.04,16.19],[21.16,0],[4,-5.4],[0,0],[-0.01,-0.72],[0,0]],"v":[[1.023,-65.856],[-0.857,-65.856],[-53.077,-18.226],[-53.967,-16.306],[-54.617,19.884],[-48.487,39.014],[-0.047,66.224],[48.443,39.024],[54.613,19.884],[54.043,-16.306],[53.153,-18.226]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-10.08,0],[-0.04,-9.55],[9.3,-0.89],[0,0],[0,0],[-0.06,9.23]],"o":[[10.15,0],[0.04,8.96],[0,0],[0,0],[-9.7,-0.51],[0.06,-9.55]],"v":[[-0.502,6.122],[17.898,23.422],[1.418,40.642],[-0.532,40.732],[-1.582,40.702],[-18.872,23.422]],"c":true},"ix":2},"nm":"路径 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":3,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.126,53.982],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状结合","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[1.51,-31.68],[-32.81,0],[0,0],[-0.52,0.5],[0,0],[-4.47,-4.34],[0,0],[-0.72,0],[0,0],[1.45,31.69],[31.63,0]],"o":[[-31.63,0],[-1.51,31.69],[0,0],[0.72,0],[0,0],[4.63,-4.05],[0,0],[0.51,0.5],[0,0],[32.82,0],[-1.45,-31.68],[0,0]],"v":[[-85.844,-57.372],[-145.854,-0.002],[-89.174,57.368],[-33.884,57.368],[-31.954,56.598],[-7.114,32.658],[9.106,33.088],[33.446,56.598],[35.376,57.368],[89.066,57.368],[145.856,-0.002],[85.956,-57.372]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-6.06,0],[0.09,-6.28],[0,0],[0,0],[5.42,-0.71],[0,0],[0,0],[-0.13,6.02],[0,0],[0,0]],"o":[[6.37,0],[0,0],[0,0],[-0.48,5.37],[0,0],[0,0],[-6.04,-0.42],[0,0],[0,0],[0.54,-5.91]],"v":[[-52.344,-25.499],[-41.004,-14.119],[-41.414,15.021],[-41.464,15.881],[-51.654,26.301],[-53.274,26.411],[-54.104,26.381],[-64.684,15.021],[-64.054,-14.119],[-64.004,-14.929]],"c":true},"ix":2},"nm":"路径 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[-6.05,0],[-0.13,-6.28],[0,0],[0,0],[5.39,-0.71],[0,0],[0,0],[0.07,6.02],[0,0],[0,0]],"o":[[6.37,0],[0,0],[0,0],[-0.31,5.37],[0,0],[0,0],[-6.06,-0.42],[0,0],[0,0],[0.35,-5.91]],"v":[[51.436,-25.499],[63.166,-14.119],[63.726,15.021],[63.716,15.881],[53.876,26.301],[52.256,26.411],[51.426,26.381],[40.466,15.021],[40.116,-14.119],[40.136,-14.929]],"c":true},"ix":2},"nm":"路径 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":3,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-62.836],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状结合","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":2,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12,"s":[0,0],"to":[0,-3.833],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[0,-23],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[0,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[0,-23],"to":[0,0],"ti":[0,-3.833]},{"t":36,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":12,"s":[100,100]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":18,"s":[100,89]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":24,"s":[100,100]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":30,"s":[100,89]},{"t":36,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状结合","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"路径","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.824,"y":1},"o":{"x":1,"y":0},"t":0,"s":[854,836.5,0],"to":[-76.667,-73.333,0],"ti":[76.667,73.333,0]},{"i":{"x":0.824,"y":0.824},"o":{"x":0.333,"y":0.333},"t":12,"s":[394,396.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[394,396.5,0],"to":[69.667,66,0],"ti":[-69.667,-66,0]},{"t":41,"s":[812,792.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.37,47.23],[198.63,111.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.62,111.24],[-180.52,47.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.24,24.23],[198.5,88.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.75,88.24],[-180.65,24.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.37,47.23],[198.63,111.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.62,111.24],[-180.52,47.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.24,24.23],[198.5,88.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.75,88.24],[-180.65,24.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"t":36,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.37,47.23],[198.63,111.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.62,111.24],[-180.52,47.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]}],"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.28627499938,0.164706006646,0.823529005051,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状结合","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.078,1.749,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.53,-0.49],[0,0],[0.01,-0.72],[0,0],[-3.98,-5.41],[-21.15,0],[-9.07,16.19],[0.11,7.13],[0,0],[0.56,0.5]],"o":[[-0.53,-0.49],[0,0],[-0.56,0.5],[0,0],[-0.12,7.12],[9.04,16.19],[21.16,0],[4,-5.4],[0,0],[-0.01,-0.72],[0,0]],"v":[[1.023,-65.856],[-0.857,-65.856],[-53.077,-18.226],[-53.967,-16.306],[-54.617,19.884],[-48.487,39.014],[-0.047,66.224],[48.443,39.024],[54.613,19.884],[54.043,-16.306],[53.153,-18.226]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-10.08,0],[-0.04,-9.55],[9.3,-0.89],[0,0],[0,0],[-0.06,9.23]],"o":[[10.15,0],[0.04,8.96],[0,0],[0,0],[-9.7,-0.51],[0.06,-9.55]],"v":[[-0.502,6.122],[17.898,23.422],[1.418,40.642],[-0.532,40.732],[-1.582,40.702],[-18.872,23.422]],"c":true},"ix":2},"nm":"路径 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":3,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.126,53.982],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状结合","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[1.51,-31.68],[-32.81,0],[0,0],[-0.52,0.5],[0,0],[-4.47,-4.34],[0,0],[-0.72,0],[0,0],[1.45,31.69],[31.63,0]],"o":[[-31.63,0],[-1.51,31.69],[0,0],[0.72,0],[0,0],[4.63,-4.05],[0,0],[0.51,0.5],[0,0],[32.82,0],[-1.45,-31.68],[0,0]],"v":[[-85.844,-57.372],[-145.854,-0.002],[-89.174,57.368],[-33.884,57.368],[-31.954,56.598],[-7.114,32.658],[9.106,33.088],[33.446,56.598],[35.376,57.368],[89.066,57.368],[145.856,-0.002],[85.956,-57.372]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-6.06,0],[0.09,-6.28],[0,0],[0,0],[5.42,-0.71],[0,0],[0,0],[-0.13,6.02],[0,0],[0,0]],"o":[[6.37,0],[0,0],[0,0],[-0.48,5.37],[0,0],[0,0],[-6.04,-0.42],[0,0],[0,0],[0.54,-5.91]],"v":[[-52.344,-25.499],[-41.004,-14.119],[-41.414,15.021],[-41.464,15.881],[-51.654,26.301],[-53.274,26.411],[-54.104,26.381],[-64.684,15.021],[-64.054,-14.119],[-64.004,-14.929]],"c":true},"ix":2},"nm":"路径 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[-6.05,0],[-0.13,-6.28],[0,0],[0,0],[5.39,-0.71],[0,0],[0,0],[0.07,6.02],[0,0],[0,0]],"o":[[6.37,0],[0,0],[0,0],[-0.31,5.37],[0,0],[0,0],[-6.06,-0.42],[0,0],[0,0],[0.35,-5.91]],"v":[[51.436,-25.499],[63.166,-14.119],[63.726,15.021],[63.716,15.881],[53.876,26.301],[52.256,26.411],[51.426,26.381],[40.466,15.021],[40.116,-14.119],[40.136,-14.929]],"c":true},"ix":2},"nm":"路径 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":3,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-62.836],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状结合","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"mm","mm":2,"nm":"合并路径 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12,"s":[0,0],"to":[0,-3.833],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[0,-23],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[0,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[0,-23],"to":[0,0],"ti":[0,-3.833]},{"t":36,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":12,"s":[100,100]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":18,"s":[100,89]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":24,"s":[100,100]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":30,"s":[100,89]},{"t":36,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"形状结合","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"路径","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.824,"y":1},"o":{"x":1,"y":0},"t":0,"s":[280,856.5,0],"to":[0,-75.333,0],"ti":[0,75.333,0]},{"i":{"x":0.598,"y":0.598},"o":{"x":0.333,"y":0.333},"t":12,"s":[280,404.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.156,"y":0},"t":30,"s":[280,404.5,0],"to":[0,75.333,0],"ti":[0,-75.333,0]},{"t":41,"s":[280,856.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.37,47.23],[198.63,111.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.62,111.24],[-180.52,47.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.24,24.23],[198.5,88.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.75,88.24],[-180.65,24.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.37,47.23],[198.63,111.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.62,111.24],[-180.52,47.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.24,24.23],[198.5,88.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.75,88.24],[-180.65,24.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]},{"t":36,"s":[{"i":[[-73.11,13.8],[-42.08,-20.93],[-20.03,-58.58],[-1.32,0.02],[0,0],[0,0],[-5.66,-5.34],[8.8,-10.94],[2.69,-1.79],[0,0],[-8.93,-17.81],[-1.07,-17.52],[91.39,-12.31],[2.13,-26.87],[27.27,3.72],[-3.37,55.45],[-12.04,26.55],[0,0],[0,0],[2.08,2.58],[-13.98,13.11],[-7.81,0.09],[-1.66,-0.24]],"o":[[-17.78,22.12],[47.78,23.41],[1.27,-0.17],[0,0],[0,0],[7.81,0.09],[13.98,13.11],[-2.15,2.66],[0,0],[-5.97,33.92],[13.57,26.73],[3.38,55.59],[-27.13,3.63],[-2.14,-26.99],[-91.02,-12.45],[1.06,-17.52],[11.87,-26.55],[0,0],[-2.6,-1.76],[-8.8,-10.94],[5.66,-5.34],[1.74,-0.01],[28.93,-84.11]],"v":[[6.44,-267.5],[55.51,-229.06],[161.73,-98.7],[165.62,-98.98],[166.05,-98.99],[166.47,-98.99],[194.59,-90.93],[183.27,-36.23],[175.9,-29.66],[176.02,-30.37],[180.37,47.23],[198.63,111.29],[48.37,210.99],[-0.23,267.5],[-49.16,210.88],[-198.62,111.24],[-180.52,47.41],[-176.2,-28.92],[-176.13,-29.81],[-183.27,-36.23],[-194.59,-90.93],[-166.47,-98.99],[-161.37,-98.65]],"c":true}]}],"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.28627499938,0.164706006646,0.823529005051,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":48,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"small loading","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[240.428,242.288,0],"ix":2},"a":{"a":0,"k":[280,280,0],"ix":1},"s":{"a":0,"k":[87,87,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,-50.782],[0,0],[50.778,0],[0,0],[0,50.781],[0,0],[-50.778,0],[0,0]],"o":[[0,0],[0,50.781],[0,0],[-50.778,0],[0,0],[0,-50.782],[0,0],[50.778,0]],"v":[[555.37,93.456],[555.37,461.248],[463.428,553.196],[95.66,553.196],[3.718,461.248],[3.718,93.456],[95.66,1.508],[463.428,1.508]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"蒙版 1"}],"w":560,"h":560,"ip":0,"op":192,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[240,240,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[99,99,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-44.183],[0,0],[44.183,0],[0,0],[0,44.183],[0,0],[-44.183,0],[0,0]],"o":[[0,0],[0,44.183],[0,0],[-44.183,0],[0,0],[0,-44.183],[0,0],[44.183,0]],"v":[[240,-160],[240,160],[160,240],[-160,240],[-240,160],[-240,-160],[-160,-240],[160,-240]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"矩形 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":192,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/single_ring.json b/crush/Crush/Src/Resource/LottieResources/single_ring.json deleted file mode 100644 index 283afc7..0000000 --- a/crush/Crush/Src/Resource/LottieResources/single_ring.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.6.9","fr":24,"ip":0,"op":48,"w":64,"h":64,"nm":"single ring","ddd":0,"assets":[{"id":"0","w":62,"h":62,"u":"","p":"data:image/webp;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"Ellipse 1.png","cl":"png","refId":"0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":48,"s":[360]}],"ix":10},"p":{"a":0,"k":[32,32,0],"ix":2},"a":{"a":0,"k":[31,31,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":48,"st":0,"bm":0}],"markers":[],"tiny":1} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/three_dots_msg_loading.json b/crush/Crush/Src/Resource/LottieResources/three_dots_msg_loading.json deleted file mode 100644 index 8b89df1..0000000 --- a/crush/Crush/Src/Resource/LottieResources/three_dots_msg_loading.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.9.6","fr":24,"ip":0,"op":24,"w":240,"h":240,"nm":"msg_loading","ddd":0,"assets":[{"id":"comp_0","nm":"circle","fr":24,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":-8,"s":[80]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[40]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":6,"s":[100]},{"t":16,"s":[50]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[-18.051,-3.051,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":-8,"s":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[40,40,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":6,"s":[80,80,100]},{"t":16,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[80,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-18.051,-3.051],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":16,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"circle 3","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,120,0],"ix":2,"l":2},"a":{"a":0,"k":[40,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":80,"h":80,"ip":8,"op":24,"st":8,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"circle 2","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[120,120,0],"ix":2,"l":2},"a":{"a":0,"k":[40,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":80,"h":80,"ip":4,"op":20,"st":4,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"circle 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,120,0],"ix":2,"l":2},"a":{"a":0,"k":[40,40,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":80,"h":80,"ip":0,"op":16,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/LottieResources/upload_image_loading.json b/crush/Crush/Src/Resource/LottieResources/upload_image_loading.json deleted file mode 100755 index 36a1fef..0000000 --- a/crush/Crush/Src/Resource/LottieResources/upload_image_loading.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.8.1","fr":24,"ip":0,"op":24,"w":24,"h":24,"nm":"加载","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"路径","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":24,"s":[360]}],"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-5.52],[-5.52,0],[0,5.52]],"o":[[-5.52,0],[0,5.52],[5.52,0],[0,0]],"v":[[0,-10],[-10,0],[0,10],[10,0]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"椭圆形","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":24,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/crush/Crush/Src/Resource/String/en.lproj/Localizable.strings b/crush/Crush/Src/Resource/String/en.lproj/Localizable.strings deleted file mode 100644 index 94cc399..0000000 --- a/crush/Crush/Src/Resource/String/en.lproj/Localizable.strings +++ /dev/null @@ -1,13 +0,0 @@ -/* - Localizable.strings - Crush - - Created by Leon on 2025/7/14. - -*/ -"welcomeWithName" = "Welcome, %@!"; -//"loginbtn.loginsignup" = "Login or Signup"; -"kuolie_no_superlike_counts2" = "Super like can let the other party find you first"; -"login.home.subtitle" = "From \"Hello\" to \"I do\", every conversation is full of heart"; -"crush.level" = "Crush Level"; -"login.personal.information" = "Personal Information"; diff --git a/crush/Crush/Src/Resource/Web/webview.js b/crush/Crush/Src/Resource/Web/webview.js deleted file mode 100644 index 08477ed..0000000 --- a/crush/Crush/Src/Resource/Web/webview.js +++ /dev/null @@ -1,172 +0,0 @@ - -if (!Object.keys) { - Object.keys = (function() { - 'use strict'; - var hasOwnProperty = Object.prototype.hasOwnProperty, - hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), - dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor' - ], - dontEnumsLength = dontEnums.length; - - return function(obj) { - if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) { - throw new TypeError('Object.keys called on non-object'); - } - - var result = [], prop, i; - - for (prop in obj) { - if (hasOwnProperty.call(obj, prop)) { - result.push(prop); - } - } - - if (hasDontEnumBug) { - for (i = 0; i < dontEnumsLength; i++) { - if (hasOwnProperty.call(obj, dontEnums[i])) { - result.push(dontEnums[i]); - } - } - } - return result; - }; - }()); -} - -if (!Array.prototype.reduce) { - Object.defineProperty(Array.prototype, 'reduce', { - value: function(callback /*, initialValue*/) { - if (this === null) { - throw new TypeError( 'Array.prototype.reduce ' + - 'called on null or undefined' ); - } - if (typeof callback !== 'function') { - throw new TypeError( callback + - ' is not a function'); - } - - var o = Object(this); - - var len = o.length >>> 0; - - var k = 0; - var value; - - if (arguments.length >= 2) { - value = arguments[1]; - } else { - while (k < len && !(k in o)) { - k++; - } - - if (k >= len) { - throw new TypeError( 'Reduce of empty array ' + - 'with no initial value' ); - } - value = o[k++]; - } - - while (k < len) { - if (k in o) { - value = callback(value, o[k], k, o); - } - - k++; - } - - return value; - } - }); -} - - -// 自定义 -window.messageHandlers = ['route', 'closeWebview', 'getUserInfo', 'setLoading', 'request','modal', 'share', 'init', 'openBrowser', 'updateActivity', 'toast']; -var EPAL = window.EPAL = {}; -var execTimeObj = {}; -window.NATIVE_CALLBACK = {}; -var originModules = window.webkit.messageHandlers; - -function createCb (method, callbacks) { - var cb = window.NATIVE_CALLBACK; - var funPath = 'window.NATIVE_CALLBACK.' + method; - - // 初始化 - var methodCb = cb[method]; - if(Object.prototype.toString.call(methodCb) !== '[object Object]') { - window.NATIVE_CALLBACK[method] = methodCb = {}; - } - - // 执行次数 - var execTime = execTimeObj[method]; - if(execTime) { - execTime++ - } else { - execTime = 1; - } - execTimeObj[method] = execTime; - - // 遍历所有回调方法 - return Object.keys(callbacks).reduce((obj, key) => { - var targetKey = key + execTime; - - // 映射赋值函数 - window.NATIVE_CALLBACK[method][targetKey] = methodCb[targetKey] = function (resp) { - callbacks[key](resp); - } - // 定义全局回调字符串 - obj[key] = funPath + '.' + targetKey; - return obj - }, {}) -} - -function nativeRun (method, args) { - var resolve = args.success, - reject = args.error; - - // 支持扩展回调函数,如分享场景 - var custom = Object.keys(args).reduce((obj, key) => { - const fun = args[key]; - if (fun != null && fun instanceof Function) { - obj[key] = fun; - } - return obj; - }, {}); - - - // 创建初始callbacks - var originCb = { - success: resolve, - error: reject, - }; - for(var attr in custom) { - originCb[attr] = custom[attr]; - } - - var nativeCb = createCb(method, originCb); - for (var key in nativeCb) { - args[key] = nativeCb[key]; - } - - if(!originModules[method] || !originModules[method].postMessage) { - throw new Error('hi man:' + method + ' was not found'); - } - originModules[method].postMessage(JSON.stringify(args)) -} - -function hanldeModule () { - messageHandlers.forEach(handler => { - EPAL[handler] = function(args) { - return nativeRun(handler, args || {}); - } - }) -} - -hanldeModule(); diff --git a/crush/Crush/Src/Utils/AppCache.swift b/crush/Crush/Src/Utils/AppCache.swift deleted file mode 100755 index cf859c0..0000000 --- a/crush/Crush/Src/Utils/AppCache.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// CacheTool.swift -// E-Wow -// -// Created by lym on 2021/1/5. -// - -import Cache -import Foundation - -// MARK: - cache keys - -enum CacheKey: String { - case user - case token - case pushToken - case ImAppkeyInfo - case keyword - case homeEnterBackgroundTime - case boolIdfaSubmmited - - // MARK: Flag - case meetGuideSeen - - // MARK: Setting - - - // MARK: Config - case userAppLanguage - case chatRedBadgeConfig - - // MARK: Chat & Friends - case friendsHeartBeatShowedOnce - - // MARK: Test - - case testRecordAIId -} - -// MARK: - config - -let diskConfig = DiskConfig(name: AppConst.bundleId) - -let dataStorage = try! Storage( - diskConfig: diskConfig, - memoryConfig: MemoryConfig(), - transformer: TransformerFactory.forData() -) - -// MARK: - AppCache - -class AppCache { - // Add objects to the cache - class func cache(key: String, value: T) { - let storage = dataStorage.transformCodable(ofType: T.self) - do { - try storage.setObject(value, forKey: key) - dlog("✅ 缓存成功:key = \(key), value = \(value)") - } catch { - dlog("❌ 缓存错误:\(error)") - } - } - - // Fetch object from the cache - class func fetchCache(key: String, type: T.Type) -> T? { - do { - let storage = dataStorage.transformCodable(ofType: type) - let value = try storage.object(forKey: key) - return value - } catch { - dlog("❌ 获取缓存失败:key = \(key), error = \(error)") - return nil - } - } - - // Remove object from the cache - class func removeCache(key: String) { - do { - try dataStorage.removeObject(forKey: key) - } catch { - dlog("❌ remove cache error = \(error)") - } - } - - // Clear cache - class func clearCache() { - do { - try dataStorage.removeAll() - } catch { - dlog("❌ clear cache erro") - } - } -} - -extension Array: Encodable, Decodable {} diff --git a/crush/Crush/Src/Utils/AppConst.swift b/crush/Crush/Src/Utils/AppConst.swift deleted file mode 100644 index e276d2f..0000000 --- a/crush/Crush/Src/Utils/AppConst.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// AppConst.swift -// Crush -// -// Created by Leon on 2025/7/13. -// - -import Foundation -struct AppConst { - /// 文档目录 - static let documentPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last! - - /// 缓存目录 - static let cachePath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last! - - /// 临时目录 - static let tempPath = NSTemporaryDirectory() as NSString - - static let schemePrefix = "crushlevel://" - - static let urlForTos = "" - static let urlForPrivacy = "" - - static let discordOAuthAppId = "1396735872459866233" - static let discordCallbackUrl = "https://test.crushlevel.ai"//"http://localhost:3000" - - /// ⚠️gg.epal.lab,需要修改成正式的账号bundleid - static let bundleId = "gg.epal.lab" - - static var h5urlRoot: String { - switch APIConfig.environment{ - case .test,.dev: - return "https://test.crushlevel.ai" - case .product,.appStore: - #warning("to do") - return "https://test.crushlevel.ai" - } - } - - - // MARK: - AI - static let gAIPhotoWidth:CGFloat = 1440.0 - static let gAIPhotoHeight:CGFloat = 2560.0 - -} diff --git a/crush/Crush/Src/Utils/AppDictManager.swift b/crush/Crush/Src/Utils/AppDictManager.swift deleted file mode 100644 index cb8a06d..0000000 --- a/crush/Crush/Src/Utils/AppDictManager.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// AppDictManager.swift -// Crush -// -// Created by Leon on 2025/7/29. -// - -import Foundation - -class AppDictManager{ - static let shared = AppDictManager() - - // 缓存键 - private enum CacheKey: String { - case aiDict = "aiDict" - case giftDict = "giftDict" - } - - var aiDict: AIDictInfo? - var aiDictApiLoading: Bool = false - - var gifts: [GiftDictModel]? - var giftDictApiLoading: Bool = false - - var chatModels : [AIChatModel]? - - init() { - // 请求最新数据 - loadAIDict(block: nil) - loadGiftDict(block: nil) - loadChatModelDict(block: nil) - // 🔥加载缓存数据 - loadCachedData() - - NotificationCenter.default.addObserver(self, selector: #selector(receiveNetStatusChanged(_:)), name: AppNotificationName.networkChanged.notificationName, object: nil) - } - - func loadRequiredDict(){ - // init() called load - } - - // MARK: - Public - - func getGiftModelBy(giftId: Int, completion: ((GiftDictModel?) -> Void)?){ - if let giftsObjs = gifts, giftsObjs.count > 0 { - // return where giftId = GiftDictModel.id - for gift in giftsObjs { - if gift.id == giftId { - completion?(gift) - return - } - } - } - loadGiftDict { ok in - if ok { - self.getGiftModelBy(giftId: giftId, completion: completion) - }else{ - completion?(nil) - } - } - } - - // MARK: - 缓存相关方法 - - /// 加载缓存数据 - private func loadCachedData() { - // 加载AI字典缓存 - if let cachedAIDict = AppCache.fetchCache(key: CacheKey.aiDict.rawValue, type: AIDictInfo.self) { - aiDict = cachedAIDict - dlog("✅ 从缓存加载AI字典成功") - } - - // 加载礼物字典缓存 - if let cachedGiftDict = AppCache.fetchCache(key: CacheKey.giftDict.rawValue, type: [GiftDictModel].self) { - gifts = cachedGiftDict - dlog("✅ 从缓存加载礼物字典成功") - } - } - - /// 缓存AI字典 - private func cacheAIDict(_ dict: AIDictInfo) { - AppCache.cache(key: CacheKey.aiDict.rawValue, value: dict) - } - - /// 缓存礼物字典 - private func cacheGiftDict(_ dict: [GiftDictModel]) { - AppCache.cache(key: CacheKey.giftDict.rawValue, value: dict) - } - - // MARK: - 网络请求方法 - - func loadAIDict(block: ((_ ok: Bool) -> Void)?){ - if aiDictApiLoading{ - block?(false) - return - } - - // 如果已有数据,直接返回成功 - if aiDict != nil { - block?(true) - return - } - - aiDictApiLoading = true - CommonProvider.request(.getAIDict, modelType: AIDictInfo.self) {[weak self] result in - self?.aiDictApiLoading = false - switch result { - case .success(let success): - self?.aiDict = success - // 缓存数据 - if let dict = success { - self?.cacheAIDict(dict) - } - if APIConfig.environment == .test { - dlog("AI Dict: \(CodableHelper.encode(success ?? AIDictInfo()) ?? Data())") - } - block?(true) - case .failure(_): - block?(false) - } - } - } - - func loadGiftDict(block: ((_ ok: Bool) -> Void)?){ - if giftDictApiLoading { - block?(false) - return - } - - // 如果已有数据,直接返回成功 - if gifts != nil { - block?(true) - return - } - - giftDictApiLoading = true - var params = [String:Any]() - var pages = [String:Any]() - pages.updateValue(1, forKey: "pn") - pages.updateValue(200, forKey: "ps") - params.updateValue(pages, forKey: "page") - CommonProvider.request(.getGiftDict(params: params), modelType: ResponseContentPageData.self) {[weak self] result in - self?.giftDictApiLoading = false - switch result { - case .success(let model): - if let datas = model?.datas{ - // dlog("所有的gift: \(datas)") - self?.gifts = datas - // 缓存数据 - self?.cacheGiftDict(datas) - } - block?(true) - case .failure: - block?(false) - } - } - } - - func loadChatModelDict(block: ((_ ok: Bool) -> Void)?){ - CommonProvider.request(.getChatModelDict, modelType: [AIChatModel].self) {[weak self] result in - switch result { - case .success(let model): - self?.chatModels = model - block?(true) - case .failure: - block?(false) - } - } - } - - // MARK: - Noti - - @objc private func receiveNetStatusChanged(_ notification: Notification) { - let status = LTNetworkManage.ltManage.status - if status != .notReachable { - if aiDict == nil { - loadAIDict(block: nil) - } - if (gifts ?? []).count == 0 { - loadGiftDict(block: nil) - } - if (chatModels ?? []).count == 0 { - loadChatModelDict(block: nil) - } - } - } - - // MARK: - 公共方法 - - /// 清除所有字典缓存 - func clearCache() { - AppCache.removeCache(key: CacheKey.aiDict.rawValue) - AppCache.removeCache(key: CacheKey.giftDict.rawValue) - aiDict = nil - gifts = nil - dlog("✅ 清除字典缓存成功") - } - - /// 强制刷新所有字典数据 - func refreshAllDicts(completion: @escaping (Bool) -> Void) { - clearCache() - loadAIDict { aiSuccess in - self.loadGiftDict { giftSuccess in - completion(aiSuccess && giftSuccess) - } - } - } -} diff --git a/crush/Crush/Src/Utils/AppNotificationName.swift b/crush/Crush/Src/Utils/AppNotificationName.swift deleted file mode 100644 index aea1591..0000000 --- a/crush/Crush/Src/Utils/AppNotificationName.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// CLNotificationName.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import Foundation - -enum AppNotificationName: String { - // MARK: Util - case networkChanged - case networkRestored // 新增:网络从无网恢复到有网的通知 - - // MARK: Base - case userInfoUpdated - case userLogout - case userLoginSuccess - - case presentSignInVc - case appLanugageChanged - case tzImagePickerNoti - - // MARK: AI Role - // AI Role - case aiRoleCreatedOrDelete - case aiRoleInfoChanged - // Album - case aiRoleAlbumPhotoInfoChanged // AI 相册信息有变更 - case aiRoleAlbumAddOrDelete - case aiRoleRelationInfoUpdated // 和AI之间的关系有更新 - - // MARK: Msg - case unreadCountChanged // ⚠️废弃,通过Combine订阅 - - // MARK: Pay & Iap & Wallet - case chargeDonePushTradeId - case walletInfoUpdated - - // MARK: Chat - case chatNaviMoreRedDotChanged - case chatPersonaUpdated - case chatSettingUpdated - case chatSettingBackgroundChanged - case chatSettingBackgroundListUpdated // 聊天背景有新生成 - - // MARK: Heartbeat - case heartbeatRelationHiddenUpdate - - // MARK: VIP - case vipStateChange - case buyCreditsOnce - - var stringValue: String { - return rawValue - } - - var notificationName: NSNotification.Name { - return NSNotification.Name(stringValue) - } - - static func post(name: AppNotificationName, object: Any? = nil, userInfo: [String: Any]? = nil) { - NotificationCenter.default.post(name: name.notificationName, object: object, userInfo: userInfo) - } -} - -extension NotificationCenter { - static func post(name: AppNotificationName, object: Any? = nil, userInfo: [String: Any]? = nil) { - NotificationCenter.default.post(name: name.notificationName, object: object, userInfo: userInfo) - } -} diff --git a/crush/Crush/Src/Utils/CodableHelper.swift b/crush/Crush/Src/Utils/CodableHelper.swift deleted file mode 100644 index 0c8c160..0000000 --- a/crush/Crush/Src/Utils/CodableHelper.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// CodableHelper.swift -// Crush -// -// Created by Leon on 2025/7/13. -// - -import Foundation - -enum CodableHelper { - - static func decode(_ type: T.Type, from data: Data) -> T? { - //try? JSONDecoder().decode(T.self, from: data) - do { - return try JSONDecoder().decode(T.self, from: data) - } catch { - dlog("❌ 解码失败: \(error)") - return nil - } - } - - static func decode(_ type: T.Type, from jsonString: String) -> T? { - guard jsonString.count > 0 else{ - // dlog("⚠️jsonString is empty") - return nil - } - guard let data = jsonString.data(using: .utf8) else { return nil } - return decode(type, from: data) - } - - static func decode(_ type: T.Type, from dictionary: [String: Any]) -> T? { - guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil } - return decode(type, from: data) - } - - static func encode(_ model: T) -> Data? { - try? JSONEncoder().encode(model) - } - - static func encodeToJSONString(_ model: T) -> String? { - guard let data = encode(model) else { return nil } - return String(data: data, encoding: .utf8) - } - - static func printJSON(_ model: T) { - if let json = encodeToJSONString(model) { - print(json) - } else { - print("⚠️ Encode to JSON failed") - } - } - - static func jsonString(from data: Data) -> String? { - String(data: data, encoding: .utf8) - } -} diff --git a/crush/Crush/Src/Utils/CommonDefine.h b/crush/Crush/Src/Utils/CommonDefine.h deleted file mode 100755 index fb2555e..0000000 --- a/crush/Crush/Src/Utils/CommonDefine.h +++ /dev/null @@ -1,44 +0,0 @@ -// -// CommonDefine.h -// LegendTeam -// -// Created by 梁博 on 13/12/21. -// - -#ifndef CommonDefine_h -#define CommonDefine_h - -// 给OC代码用的宏定义 - -#define WeakSelf autoreleasepool{} __weak typeof(self) weakSelf = self -#define StrongSelf autoreleasepool{} __strong typeof(self) self = weakSelf -#define ScreenWidth [UIScreen mainScreen].bounds.size.width -#define ScreenHeight [UIScreen mainScreen].bounds.size.height - -#define StatusBarHeight UIWindow.statusBarHeight -#define NavigationBarHeight 44.f -#define StatusAndNaviBarHeight (StatusBarHeight + NavigationBarHeight) -#define BottomInset UIWindow.safeAreaBottom - -#define TabBarTotalHeight UIWindow.tabBarTotalHeight - -/// 获取本地字符串 -static NSString *const tableName = @"Localizable"; -#define LSF(key) [OCLanuagesUtils localizedWithKey:key tableName:tableName arguments:nil] -#define LSFA(key,arguments) [OCLanuagesUtils localizedWithKey:key tableName:tableName arguments:arguments] - - -#ifndef __OPTIMIZE__ -#define DLog(...) NSLog(__VA_ARGS__); -#define RLog(...) NSLog(__VA_ARGS__) -#else -#define DLog(...) -#define RLog(...) NSLog(__VA_ARGS__) -#endif - -#define StrNoneNull(s) (s ?: @"") -#define TabBarInset BottomInset - -#define kBackgroundColor(x) [UIColor colorWithRed:18/255.0 green:14/255.0 blue:27/255.0 alpha:x] - -#endif /* CommonDefine_h */ diff --git a/crush/Crush/Src/Utils/Extensions/AttributeString.swift b/crush/Crush/Src/Utils/Extensions/AttributeString.swift deleted file mode 100644 index 7d7ae4f..0000000 --- a/crush/Crush/Src/Utils/Extensions/AttributeString.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// AttributeString.swift -// Crush -// -// Created by Leon on 2025/8/26. -// - -/// 业务经常用到的一些AttributeString生成方法 -extension NSAttributedString{ - - /// 图片+文字的AttribueString。默认黑色vip+黑色文字 - static func getIconTitleAttributeByWords( - words: String, - icon: IconCode? = nil, - iconImage: UIImage? = nil, - iconSize: CGSize = CGSize(width: 24, height: 24), - iconColor: UIColor = .black, - textFont: UIFont = .t.tbsl, - textColor: UIColor = .black - ) -> NSAttributedString { - let attributedString = NSMutableAttributedString() - - // 公共方法:生成 icon 的 NSTextAttachment - func makeAttachment(with image: UIImage) -> NSAttributedString { - let attachment = NSTextAttachment() - attachment.image = image - - // 计算垂直偏移,使图标和文字基线对齐 - let yOffset = (textFont.capHeight - iconSize.height) / 2 - attachment.bounds = CGRect( - x: 0, - y: yOffset, - width: iconSize.width, - height: iconSize.height - ) - return NSAttributedString(attachment: attachment) - } - - // 1. 如果传了外部 image - if let customImage = iconImage { - attributedString.append(makeAttachment(with: customImage)) - } - // 2. 否则使用 iconFont 生成的 image - else if let iconCode = icon, let fontImage = MWIconFont.image(fromIcon: iconCode, size: iconSize, color: iconColor) { - attributedString.append(makeAttachment(with: fontImage)) - } - - // 3. 拼接文字 - let text = " \(words)" - let textAttributedString = NSAttributedString( - string: text, - attributes: [ - .font: textFont, - .foregroundColor: textColor - ] - ) - attributedString.append(textAttributedString) - - return attributedString - } - -} diff --git a/crush/Crush/Src/Utils/Extensions/BundleExt.swift b/crush/Crush/Src/Utils/Extensions/BundleExt.swift deleted file mode 100755 index 1958d15..0000000 --- a/crush/Crush/Src/Utils/Extensions/BundleExt.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Bundle+Ext.swift -// DouYinSwift5 -// -// Created by lym on 2020/7/23. -// Copyright © 2020 lym. All rights reserved. -// - -import Foundation - -public extension Bundle { - /// App命名空间 - static var namespace: String { - return (Bundle.main.infoDictionary?["CFBundleExecutable"] as? String) ?? "" - } - - /// App 名称 - static var appDisplayName: String { - return (Bundle.main.infoDictionary!["CFBundleDisplayName"] as? String) ?? "" - } - - /// 应用名称 - static var appBundleName: String { - return (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) ?? "" - } - - /// 应用ID - static var appBundleID: String { - return (Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String) ?? "" - } - - /// 应用版本号 - static var appVersion: String { - return (Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? "" - } - - /// 应用构建版本号 - static var appBuildVersion: String? { - return (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) ?? "" - } - - /// iOS 获取设备当前语言的代码 - static var appLanguage: String? { - return Bundle.main.preferredLocalizations.first - } -} diff --git a/crush/Crush/Src/Utils/Extensions/CollectionExt.swift b/crush/Crush/Src/Utils/Extensions/CollectionExt.swift deleted file mode 100755 index a754f3d..0000000 --- a/crush/Crush/Src/Utils/Extensions/CollectionExt.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Collection+Ext.swift -// E-Wow -// -// Created by lym on 2021/1/19. -// - -import Foundation - -public extension Collection { - subscript(safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil - } -} - -extension Array { - static func realEmpty(array: Array?) -> Bool { - return array == nil || array?.count == 0 - } -} diff --git a/crush/Crush/Src/Utils/Extensions/CombineExt.swift b/crush/Crush/Src/Utils/Extensions/CombineExt.swift deleted file mode 100644 index 17acae8..0000000 --- a/crush/Crush/Src/Utils/Extensions/CombineExt.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// CombineExt.swift -// Crush -// -// Created by Leon on 2025/8/7. -// - -import Combine -import UIKit - -extension UITextField { - var textPublisher: AnyPublisher { - NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: self) - .map { ($0.object as? UITextField)?.text } - .eraseToAnyPublisher() - } -} - -extension UITextView { - var textPublisher: AnyPublisher { - NotificationCenter.default - .publisher(for: UITextView.textDidChangeNotification, object: self) - .map { ($0.object as? UITextView)?.text } - .eraseToAnyPublisher() - } -} - -extension Publishers { - /* - 使用: - Publishers.eitherLatest(container.$whoIm, container.$birthdayDate) - .sink { whoIm, birthday in - // whoIm: String? - // birthday: Date? - print("更新 -> whoIm=\(whoIm ?? "nil"), birthday=\(birthday?.description ?? "nil")") - } - .store(in: &cancellables) - - */ - /// 当任一上游(输出为 Optional)更新时触发,输出为两者的“最新快照” - static func eitherLatest( - _ p1: P1, - _ p2: P2 - ) -> AnyPublisher<(A?, B?), Never> - where P1.Output == A?, P2.Output == B?, P1.Failure == Never, P2.Failure == Never { - - let left = p1.map { value -> (A?, B?) in (value, nil as B?) } - let right = p2.map { value -> (A?, B?) in (nil as A?, value) } - - return Publishers.Merge(left, right) - // 记住历史最新值:新的非 nil 覆盖对应位;nil 则沿用旧值 - .scan((nil as A?, nil as B?)) { last, new in - (new.0 ?? last.0, new.1 ?? last.1) - } - .eraseToAnyPublisher() - } -} - diff --git a/crush/Crush/Src/Utils/Extensions/CommonExt.swift b/crush/Crush/Src/Utils/Extensions/CommonExt.swift deleted file mode 100644 index eb19f25..0000000 --- a/crush/Crush/Src/Utils/Extensions/CommonExt.swift +++ /dev/null @@ -1,355 +0,0 @@ -// -// Common+Ext.swift -// Crush -// -// Created by Leon on 2025/7/11. -// - -import Foundation - -/// 打印 -//func dlog(_ message: T, file: StaticString = #file, method: String = #function, line: Int = #line) { -// #if DEBUG -// let fileName = (file.description as NSString).lastPathComponent -// print("🔹\(fileName) \(method)[\(line)]:\n\(message)\n") -// #endif -//} - -func dlog(_ message: T, file: StaticString = #file, method: String = #function, line: Int = #line) { - #if DEBUG - let fileName = (file.description as NSString).lastPathComponent - print("🔹\(fileName) \(method)[\(line)]:\n\(message)\n") - #endif -} - -struct EmptyModel: Codable {} - - -// MARK: - DecodableValue -enum DecodableValue: Decodable, Encodable { - case string(String) - case int(Int) - case double(Double) - case bool(Bool) - case array([DecodableValue]) - case dictionary([String: DecodableValue]) - case null - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if container.decodeNil() { - self = .null - } else if let b = try? container.decode(Bool.self) { - self = .bool(b) - } else if let i = try? container.decode(Int.self) { - self = .int(i) - } else if let d = try? container.decode(Double.self) { - self = .double(d) - } else if let s = try? container.decode(String.self) { - self = .string(s) - } else if let a = try? container.decode([DecodableValue].self) { - self = .array(a) - } else if let dict = try? container.decode([String: DecodableValue].self) { - self = .dictionary(dict) - } else { - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Unsupported value" - ) - } - } - - - // MARK: - Encodable - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch self { - case .null: - try container.encodeNil() - case .bool(let b): - try container.encode(b) - case .int(let i): - try container.encode(i) - case .double(let d): - try container.encode(d) - case .string(let s): - try container.encode(s) - case .array(let a): - try container.encode(a) - case .dictionary(let d): - try container.encode(d) - } - } -} - -extension Encodable { - func toNonNilDictionary() -> [String: Any] { - return Self._toDictionary(value: self) - } - - private static func _toDictionary(value: Any) -> [String: Any] { - var result: [String: Any] = [:] - let mirror = Mirror(reflecting: value) - - for child in mirror.children { - guard let key = child.label else { continue } - - let rawValue = child.value - let valueMirror = Mirror(reflecting: rawValue) - - // 处理 Optional 类型 - if valueMirror.displayStyle == .optional { - if let some = valueMirror.children.first?.value { - let processedValue = processValue(some) - if !isNil(processedValue) { - result[key] = processedValue - } - } - continue - } - - // 处理值 - let processedValue = processValue(rawValue) - if !isNil(processedValue) { - result[key] = processedValue - } - } - - return result - } - - private static func processValue(_ value: Any) -> Any { - if let rawRepresentable = value as? (any RawRepresentable) { - return rawRepresentable.rawValue - } - - // 处理特定类型(如 Sex 枚举) - if let sex = value as? Sex { - return sex.rawValue - } - - // 处理 Encodable 类型(嵌套结构体或类) - if let encodableValue = value as? Encodable { - let mirror = Mirror(reflecting: encodableValue) - - // 处理数组或集合 - if mirror.displayStyle == .collection { - return mirror.children.compactMap { child in - let processed = processValue(child.value) - return isNil(processed) ? nil : processed - } - } - - // 处理嵌套结构体或类 - if mirror.displayStyle == .class || mirror.displayStyle == .struct || mirror.displayStyle == .set { - return _toDictionary(value: encodableValue) - } - } - - return value - } - - // 辅助函数:检查值是否为 nil - private static func isNil(_ value: Any) -> Bool { - let mirror = Mirror(reflecting: value) - return mirror.displayStyle == .optional && mirror.children.isEmpty - } - -// func toNonNilDictionary() -> [String: Any] { -// return Self._toDictionary(value: self) -// } -// private static func _toDictionary(value: Any) -> [String: Any] { -// var result: [String: Any] = [:] -// let mirror = Mirror(reflecting: value) -// -// for child in mirror.children { -// guard let key = child.label else { continue } -// -// let rawValue = child.value -// let valueMirror = Mirror(reflecting: rawValue) -// -// // Optional 类型处理 -// if valueMirror.displayStyle == .optional { -// if let some = valueMirror.children.first?.value { -// result[key] = processValue(some) -// } -// } else { -// result[key] = processValue(rawValue) -// } -// } -// -// return result -// } -// -// private static func processValue(_ value: Any) -> Any { -// let mirror = Mirror(reflecting: value) -// -// // 递归处理 Optional -// if mirror.displayStyle == .optional { -// if let some = mirror.children.first?.value { -// return processValue(some) // 递归处理解包后的值 -// } else { -// return ""// NSNull() // 或者直接 return "" 视你的需求 -// } -// } -// -// if let sex = value as? Sex { -// return sex.rawValue -// } -// -// if let encodableValue = value as? Encodable { -// let mirror = Mirror(reflecting: encodableValue) -// -// if mirror.displayStyle == .class || mirror.displayStyle == .struct || mirror.displayStyle == .set { -// return _toDictionary(value: encodableValue) -// } -// } -// -// return value -// } - - // MARK: - part 2 - - /// 将结构体中指定 key 的字段(如果非 nil)导出为字典 - func toPartialDictionary(keys includedKeys: Set = []) -> [String: Any] { - return Self._toDictionary(value: self, onlyIncludeKeys: includedKeys) - } - - private static func _toDictionary(value: Any, onlyIncludeKeys: Set? = nil) -> [String: Any] { - var result: [String: Any] = [:] - let mirror = Mirror(reflecting: value) - - for child in mirror.children { - guard let key = child.label else { continue } - - if let allowedKeys = onlyIncludeKeys, !allowedKeys.contains(key) { - continue - } - - let rawValue = child.value - let valueMirror = Mirror(reflecting: rawValue) - - if valueMirror.displayStyle == .optional { - if let some = valueMirror.children.first?.value { - result[key] = processValue(some, onlyIncludeKeys: nil) - } - } else { - result[key] = processValue(rawValue, onlyIncludeKeys: nil) - } - } - - return result - } - - /// 支持将Sex输出为int - private static func processValue(_ value: Any, onlyIncludeKeys: Set?) -> Any { - let mirror = Mirror(reflecting: value) - - if mirror.displayStyle == .struct { - return _toDictionary(value: value, onlyIncludeKeys: onlyIncludeKeys) - } else if let rawRepresentable = value as? any RawRepresentable { - return rawRepresentable.rawValue - } else { - return value - } - } -} - - -extension Array where Element: Equatable { - mutating func removeObj(_ object: Element) { - if let index = firstIndex(of: object) { - remove(at: index) - } - } -} - -// MARK: - 通用扩展 - -extension Optional { - /// 是否为 nil - var isNil: Bool { self == nil } - - /// 是否有值 - var isSome: Bool { self != nil } -} - -extension Optional where Wrapped == Int { - /// 是否大于 0 - var isPositive: Bool { (self ?? 0) > 0 } - - /// 是否为负数 - var isNegative: Bool { (self ?? 0) < 0 } - - /// 是否为 nil 或 0 - var isNilOrZero: Bool { self == nil || self == 0 } - - /// 转字符串(nil → "") - var stringValue: String { self.map { "\($0)" } ?? "" } -} - -extension Optional where Wrapped == String { - /// 是否为空串或 nil - var isNilOrEmpty: Bool { self?.isEmpty ?? true } - - /// 去除空格后的安全字符串 - var trimmed: String { self?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } - - /// 转 Int(失败或 nil 返回 nil) - var intValue: Int? { self.flatMap { Int($0) } } - - /// 转 Double(失败或 nil 返回 nil) - var doubleValue: Double? { self.flatMap { Double($0) } } -} - -extension Optional where Wrapped == Bool { - /// 默认 false - var boolValue: Bool { self ?? false } - - /// 默认 true - var trueOrNil: Bool { self ?? true } -} - -extension Double { - func formatted(decimal: Int = 2, asPercent: Bool = false, usesGroupingSeparator: Bool = true) -> String { -// let formatter = NumberFormatter() -// formatter.minimumFractionDigits = decimal -// formatter.maximumFractionDigits = decimal -// formatter.numberStyle = .decimal -// return formatter.string(from: NSNumber(value: self)) ?? "\(self)" - - let value = asPercent ? self * 100 : self - - let formatter = NumberFormatter() - formatter.minimumFractionDigits = decimal - formatter.maximumFractionDigits = decimal - formatter.usesGroupingSeparator = usesGroupingSeparator - formatter.numberStyle = .decimal - - let result = formatter.string(from: NSNumber(value: value)) ?? "\(value)" - return asPercent ? result + "%" : result - } -} - -extension CGFloat { - func formatted(decimal: Int = 2, asPercent: Bool = false, usesGroupingSeparator: Bool = true) -> String { - let value = asPercent ? self * 100 : self - - let formatter = NumberFormatter() - formatter.minimumFractionDigits = decimal - formatter.maximumFractionDigits = decimal - formatter.usesGroupingSeparator = usesGroupingSeparator - formatter.numberStyle = .decimal - - let result = formatter.string(from: NSNumber(value: value)) ?? "\(value)" - return asPercent ? result + "%" : result - } -} - -extension Dictionary { - func merged(with other: Dictionary, preferNew: Bool = true) -> Dictionary { - return self.merging(other) { old, new in preferNew ? new : old } - } -} diff --git a/crush/Crush/Src/Utils/Extensions/DateExt.swift b/crush/Crush/Src/Utils/Extensions/DateExt.swift deleted file mode 100644 index f74f565..0000000 --- a/crush/Crush/Src/Utils/Extensions/DateExt.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// DateExt.swift -// Crush -// -// Created by Leon on 2025/7/15. -// - -import Foundation -import SwiftDate - -extension Date { - /// 获取当前 时间戳 - 默认毫秒 - var timeStamp: Int { - let timeInterval: TimeInterval = timeIntervalSince1970 - let millisecond = Int(round(timeInterval * 1000)) - return millisecond - } - - var timeStampInSeconds: Int { - let timeInterval: TimeInterval = timeIntervalSince1970 - let timeStamp = Int(timeInterval) - return timeStamp - } - - /// 获取当前 毫秒级 时间戳 - 13位 - var milliStamp: Int { - let timeInterval: TimeInterval = timeIntervalSince1970 - let millisecond = Int(round(timeInterval * 1000)) - return millisecond - } - - func toString(dateFormat: String = "yyyy-MM-dd HH:mm:ss") -> String { - let fm = DateFormatter() - fm.dateFormat = dateFormat - fm.locale = Locale(identifier: Languages.localRegionCode()) - return fm.string(from: self) - } - - static func dateFromMilliseconds(_ ms: Int64) -> Date { - return Date(timeIntervalSince1970: TimeInterval(ms) / 1000) - } - - static func timerStyle(style: TimerStyle, millisecond: Int) -> String { - return Date.timerStyle(style: style, second: millisecond / 1000) - } - - /// second: 秒 - static func timerStyle(style: TimerStyle, second: Int) -> String { - if second == 0 { - return "" - } - let interval = TimeInterval(second) - let date = Date(timeIntervalSince1970: interval) - switch style { - case .WDMYHMS: - return date.toString(dateFormat: "E, d MMM yyyy HH:mm:ss") - case .DMYHMS: - return date.toString(dateFormat: "d MMM yyyy HH:mm:ss") - case .DMYHM: - return date.toString(dateFormat: "d MMM yyyy HH:mm") - case .DMHM: - return date.toString(dateFormat: "d MMM HH:mm") - case .DMY: - return date.toString(dateFormat: "d MMM yyyy") - case .DM: - return date.toString(dateFormat: "d MMM") - case .MY: - return date.toString(dateFormat: "MMM yyyy") - case .HM: - return date.toString(dateFormat: "HH:mm") - case .IMLIST: - return Date.timerStyleForIM(style: .IMLIST, second: second) - case .IMCHAT: - return Date.timerStyleForIM(style: .IMCHAT, second: second) - case .EVENTEND: - return date.toString(dateFormat: "d MMM HH:mm") - default: - return "-" - } - } - - /// IM类型的时间格式 xx(分钟\小时\天\周\月\年)前 - /// - Parameter timeStamp: 时间戳 13位 - /// - Returns: String - private static func timerStyleForIM(style: TimerStyle, second: Int) -> String { - let interval = TimeInterval(second) - let last = Date(timeIntervalSince1970: interval) - // 今天 - if last.isToday { - return last.toString(dateFormat: "HH:mm") - } - - switch style { - case .IMLIST: - // 昨天 - if last.isYesterday { - return "Yesterday" - } - // 本年不带年份 - if last.compare(.isThisYear) { - return last.toString(dateFormat: "d MMM") - } - return last.toString(dateFormat: "d MMM yyyy") - - case .IMCHAT: - // 昨天 - if last.isYesterday { - return "Yesterday" + " " + last.toString(dateFormat: "HH:mm") - } - // 本年不带年份 - if last.compare(.isThisYear) { - return last.toString(dateFormat: "d MMM HH:mm") - } - return last.toString(dateFormat: "d MMM yyyy HH:mm") - - default: - return "" - } - } -} - -enum TimerStyle { - case WDMYHMS - case DMYHMS - case DMYHM - case DMHM - case DMY - case DM - case MY - case HM - case IMLIST - case IMCHAT - case PUBLISH - case COMMENT - case EVENTEND -} - - -extension TimeInterval { - /// 格式化录音时长:<1分钟显示s,>=1分钟显示 m:ss - var imAIaudioDurationString: String { -// let totalSeconds = Int(self) -// let minutes = totalSeconds / 60 -// let seconds = totalSeconds % 60 -// -// if minutes > 0 { -// // 显示分钟:秒(秒补零) -// return String(format: "%d:%02d''", minutes, seconds) -// } else { -// // 小于1分钟只显示秒 -// return "\(seconds)''" -// } - - if self < 1 { - return "1''" - } - - let totalSeconds = Int(self.rounded()) - - if totalSeconds < 60 { - return "\(totalSeconds)''" - } - - let minutes = totalSeconds / 60 - let seconds = totalSeconds % 60 - - if seconds == 0 { - return "\(minutes)'" - } - - return String(format: "%d'%02d''", minutes, seconds) - } -} diff --git a/crush/Crush/Src/Utils/Extensions/NSNumberExt.swift b/crush/Crush/Src/Utils/Extensions/NSNumberExt.swift deleted file mode 100755 index 4d5a758..0000000 --- a/crush/Crush/Src/Utils/Extensions/NSNumberExt.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// NSNumber+Ext.swift -// SwiftExtensions -// -// Created by lym on 2021/4/15. -// - -import Foundation - -extension NSNumber { - @objc func displayCount() -> String { - if doubleValue <= 0 { - return "0" - } - if doubleValue < 1000 { - return description - } - if doubleValue >= 999999 { - return "999.9K+" - } - let result = doubleValue / 1000.0 - let num1 = NSNumber(value: result) - let numberFormatter = NumberFormatter() - numberFormatter.maximumFractionDigits = 1 - numberFormatter.roundingMode = .down - numberFormatter.positiveFormat = "#0.0" - return numberFormatter.string(from: num1)! + "K" - } - -// @objc func displayMoney() -> String { -// let numberFormatter = NumberFormatter() -// numberFormatter.maximumFractionDigits = 0 -// numberFormatter.numberStyle = .decimal -// numberFormatter.locale = Locale(identifier: Languages.localRegionCode()) -// return numberFormatter.string(from: self)! -// } -} - -extension Int { -// func displayMoney() -> String { -// return NSNumber(value: self).displayMoney() -// } -} diff --git a/crush/Crush/Src/Utils/Extensions/StringExt.swift b/crush/Crush/Src/Utils/Extensions/StringExt.swift deleted file mode 100755 index 9503f00..0000000 --- a/crush/Crush/Src/Utils/Extensions/StringExt.swift +++ /dev/null @@ -1,421 +0,0 @@ -// -// UIWindow+Ext.swift -// DouYinSwift5 -// -// Created by lym on 2020/7/23. -// Copyright © 2020 lym. All rights reserved. -// - -import Foundation -import UIKit - -public extension String { - static func randomString(_ length: Int) -> String { - let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - return String((0 ..< length).map { _ in letters.randomElement()! }) - } - - init(deviceToken: Data) { - self = deviceToken.map { String(format: "%.2hhx", $0) }.joined() - } - - /// 是否包含 -// func contains(_ regular: String) -> Bool { -// return range(of: regular, options: .regularExpression, range: nil, locale: nil) != nil -// } - - /// 去掉字符串首尾的空格换行,中间的空格和换行忽略 - var trimmed: String { - return trimmingCharacters(in: .whitespacesAndNewlines) - } - - func removeSpaceAndNewLine() -> String { - return trimmingCharacters(in: .whitespacesAndNewlines) - } - - var removingAllWhitespace: String { - return replacingOccurrences(of: " ", with: "") - } - - /// 是否不为空 - /// - /// "", " ", "\n", " \n "都视为空 - /// 不为空返回true, 为空返回false - var isNotBlank: Bool { - return !trimmed.isEmpty - } - - /// 字符串的全部范围 - var rangeOfAll: NSRange { - return NSRange(location: 0, length: count) - } - - /// 是否为 nil 或者为 "" - static func realEmpty(str: String?) -> Bool { - return (str == nil || str == "") - } - - /// JSON 字符串 转换为 字典 - func toDictionary() -> [String: Any]? { - guard let jsonData = data(using: .utf8), - let dict = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) - else { - return nil - } - return (dict as! [String: Any]) - } - - /// JSON 字符串 转换为 Array - func jsonStringToArray() -> [Any]? { - guard let jsonData: Data = data(using: .utf8), - let array = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) - else { - return nil - } - return (array as! [Any]) - } - - /// 获取范围 - @discardableResult - func matchStrRange(_ matchStr: String) -> [NSRange] { - var selfStr = self as NSString - var withStr = Array(repeating: "X", count: (matchStr as NSString).length).joined(separator: "") // 辅助字符串 - if matchStr == withStr { withStr = withStr.lowercased() } // 临时处理辅助字符串差错 - var allRange = [NSRange]() - while selfStr.range(of: matchStr).location != NSNotFound { - let range = selfStr.range(of: matchStr) - allRange.append(NSRange(location: range.location, length: range.length)) - selfStr = selfStr.replacingCharacters(in: NSMakeRange(range.location, range.length), with: withStr) as NSString - } - return allRange - } - - /// 数字和字母混合 - static func randomAlphaNumericString(length: Int) -> String { - let allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - let allowedCharsCount = UInt32(allowedChars.count) - var randomString = "" - - for _ in 0 ..< length { - let randomNum = Int(arc4random_uniform(allowedCharsCount)) - let randomIndex = allowedChars.index(allowedChars.startIndex, offsetBy: randomNum) - let newCharacter = allowedChars[randomIndex] - randomString += String(newCharacter) - } - - return randomString - } - - /// 生成指定长度的随机数字字符串 - static func randomNumber(length: Int) -> String { - var result = "" - for _ in 0.. String { - return NSLocalizedString(self, tableName: tableName, bundle: bundle, comment: "") - } -} - -// MAKR : 正则分割 -// 定义一个结构体来承载分割后的结果 -struct SplitedResult { - let fragment: String - let isMatched: Bool - let captures: [String?] -} - -extension String { - /// 正则分割字符串 - func split( - usingRegex pattern: String, - options: NSRegularExpression.Options = .dotMatchesLineSeparators - ) -> [SplitedResult] { - do { - let regex = try NSRegularExpression(pattern: pattern, options: options) - let matches = regex.matches(in: self, options: [], range: NSRange(location: 0, length: utf16.count)) - - var currentIndex = startIndex - var range: Range - var captures: [String?] = [] - var results: [SplitedResult] = [] - for match in matches { - range = Range(match.range, in: self)! - if range.lowerBound > currentIndex { - results.append(SplitedResult(fragment: String(self[currentIndex ..< range.lowerBound]), isMatched: false, captures: [])) - } - - if match.numberOfRanges > 1 { - for i in 1 ..< match.numberOfRanges { - if let _range = Range(match.range(at: i), in: self) { - captures.append(String(self[_range])) - } else { - captures.append(nil) - } - } - } - - results.append(SplitedResult(fragment: String(self[range]), isMatched: true, captures: captures)) - currentIndex = range.upperBound - captures.removeAll() - } - - if endIndex > currentIndex { - results.append(SplitedResult(fragment: String(self[currentIndex ..< endIndex]), isMatched: false, captures: [])) - } - - return results - } catch { - fatalError("正则表达式有误,请更正后再试!") - } - } -} - -extension String { - // base64 - func encodeBase64() -> String { - let plainData = data(using: String.Encoding.utf8) - let base64String = plainData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) - return base64String! - } - - // - func decodeBase64() -> String { - let decodedData = NSData(base64Encoded: self, options: NSData.Base64DecodingOptions(rawValue: 0)) - let decodedString = NSString(data: decodedData! as Data, encoding: String.Encoding.utf8.rawValue)! as String - return decodedString - } -} - -extension String { - /// 计算字体宽度 字体大小 - public func textWidth(font: UIFont) -> CGFloat { - let str = self as NSString - let size = CGSize(width: 20000, height: 100) - let attributes = [NSAttributedString.Key.font: font] - let labelSize = str.boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil).size - return labelSize.width - } - - /// 计算指定字体的尺寸 - /// - /// - Parameters: - /// - font: 字体 - /// - size: 区域大小 - /// - lineBreakMode: 换行模式 - /// - Returns: 尺寸 - func size(for font: UIFont, size: CGSize, lineBreakMode: NSLineBreakMode) -> CGSize { - var attr: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font] - if lineBreakMode != .byWordWrapping { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineBreakMode = lineBreakMode - attr[.paragraphStyle] = paragraphStyle - } - let rect = (self as NSString).boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attr, context: nil) - return rect.size - } -} - -// MARK: - Extensions - -extension String { - var isValidEmail: Bool { - let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - let emailPred = NSPredicate(format: "SELF MATCHES %@", emailPattern) - return emailPred.evaluate(with: self) - } - - var isValidNickname: Bool{ - return self.trimmed.count >= 2 - } -} - -extension String { - /// Displays a number with B, M, K suffixes based on magnitude - static func displayNumber(_ num: NSNumber, scale: Int) -> String { - if num.doubleValue == 0 { - return "0" - } - - let dm = NSDecimalNumber(decimal: num.decimalValue) - let handler = NSDecimalNumberHandler( - roundingMode: .down, - scale: Int16(scale), - raiseOnExactness: false, - raiseOnOverflow: false, - raiseOnUnderflow: false, - raiseOnDivideByZero: true - ) - - let dm_1 = NSDecimalNumber(string: "1.0") - let dm_1000 = NSDecimalNumber(string: "1000.0") - let dm_1000000 = NSDecimalNumber(string: "1000000.0") - let dm_1000000000 = NSDecimalNumber(string: "1000000000.0") - - let formatter = NumberFormatter() - formatter.maximumFractionDigits = scale - formatter.numberStyle = .decimal - formatter.roundingMode = .down - formatter.locale = Locale.current // Assuming LanguagesUtils.locale() returns current locale - - switch num.doubleValue { - case let value where value >= 1000000000: - let number = dm.dividing(by: dm_1000000000, withBehavior: handler) - guard let res = formatter.string(from: number) else { return "" } - return "\(res)B" - - case let value where value >= 1000000: - let number = dm.dividing(by: dm_1000000, withBehavior: handler) - guard let res = formatter.string(from: number) else { return "" } - return "\(res)M" - - case let value where value >= 1000: - let number = dm.dividing(by: dm_1000, withBehavior: handler) - guard let res = formatter.string(from: number) else { return "" } - return "\(res)K" - - default: - let number = dm.dividing(by: dm_1, withBehavior: handler) - guard let res = formatter.string(from: number) else { return "" } - return res - } - } - - /// Displays double with K rule, showing one decimal place for large numbers - static func displayDouble(_ num: Double) -> String { - if num < 1000.0 { - return displayNumber(NSNumber(value: num), scale: 0) - } - return displayNumber(NSNumber(value: num), scale: 1) - } - - /// Displays integer with K rule, defaultscale is 0 - static func displayInteger(_ num: Int) -> String { - displayNumber(NSNumber(value: num), scale: 0) - } - - /// Displays timer from milliseconds - static func displayTimer(_ ms: Int) -> String { - let day = ms / (1000 * 60 * 60 * 24) - let hour = (ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) - let minute = (ms % (1000 * 60 * 60)) / (1000 * 60) - let second = (ms % (1000 * 60)) / 1000 - - switch (day, hour, minute) { - case let (d, _, _) where d >= 1: - return String(format: "%ldd %ldh", day, hour) - case let (0, h, m) where h > 0: - return String(format: "%ldh %ldm", hour, m) - case let (0, 0, m) where m >= 0: - return String(format: "%02ld:%02ld", m, second) - default: - return String(format: "%02ld:%02ld", minute, second) - } - } - - /// Formats integer with thousand separator - static func thousandString(_ num: Int) -> String { - let formatter = NumberFormatter() - formatter.maximumFractionDigits = 0 - formatter.numberStyle = .decimal - formatter.locale = Locale.current - return formatter.string(from: NSNumber(value: num)) ?? "" - } - - /// Formats double with thousand separator, two decimal places - static func thousandString(float num: Double) -> String { - thousandString(float: num, maxDigits: 2) - } - - /// Converts formatted string back to double - static func float(fromThousandString string: String) -> Double { - let formatter = NumberFormatter() - formatter.maximumFractionDigits = 2 - formatter.numberStyle = .decimal - formatter.locale = Locale.current - return formatter.number(from: string)?.doubleValue ?? 0.0 - } - - /// Formats double with thousand separator and specified decimal places - static func thousandString(float num: Double, maxDigits: Int) -> String { - thousandString(float: num, maxDigits: maxDigits, roundingMode: .down) - } - - /// Formats double with thousand separator, specified decimal places and rounding mode - static func thousandString(float num: Double, maxDigits: Int, roundingMode: NumberFormatter.RoundingMode) -> String { - thousandString(float: num, maxDigits: maxDigits, locale: Locale.current, roundingMode: roundingMode) - } - - /// Formats double with thousand separator, specified decimal places, locale, and rounding mode - static func thousandString(float num: Double, maxDigits: Int, locale: Locale, roundingMode: NumberFormatter.RoundingMode) -> String { - let formatter = NumberFormatter() - formatter.maximumFractionDigits = maxDigits - formatter.numberStyle = .decimal - formatter.roundingMode = roundingMode - formatter.locale = locale - return formatter.string(from: NSNumber(value: num)) ?? "" - } - - /// Formats number with ordinal suffix (st, nd, rd, th) - static func numberSequence(_ num: Int) -> String { - let subNum = num % 10 - if num > 10 && num < 20 { - return "\(num)th" - } - - switch subNum { - case 1: - return "\(num)st" - case 2: - return "\(num)nd" - case 3: - return "\(num)rd" - default: - return "\(num)th" - } - } - - /// 将括号以及括号内的文字的range全部输出 - static func findBracketRanges(in text: String) -> [NSRange] { - // 匹配英文 () 或中文 () - let pattern = "(\\([^)]*\\))|(([^)]*))" - - guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { - print("Invalid regex pattern") - return [] - } - - let nsString = text as NSString - let range = NSRange(location: 0, length: nsString.length) - - var ranges: [NSRange] = [] - regex.enumerateMatches(in: text, options: [], range: range) { (match, _, _) in - if let matchRange = match?.range { - ranges.append(matchRange) - } - } - - return ranges - } - - /// 将()()中的文字删除后,剩余的文字 - static func removeBracketContents(from text: String) -> String { - let ranges = findBracketRanges(in: text) - guard !ranges.isEmpty else { return text } - - let nsString = text as NSString - let mutable = NSMutableString(string: text) - - // 倒序删除,避免 range 偏移 - for range in ranges.reversed() { - mutable.deleteCharacters(in: range) - } - - return String(mutable) - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UIButton+EG.h b/crush/Crush/Src/Utils/Extensions/UIButton+EG.h deleted file mode 100644 index d29a38e..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIButton+EG.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// UIButton+EG.h -// EGCommon -// -// Created by donglyu on 2020/4/9. -// Copyright © 2020 Company. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -/* - 针对同时设置了Image和Title的场景时UIButton中的图片和文字的关系 - */ -typedef NS_ENUM(NSInteger, ButtonImageTitleStyle ) { - ButtonImageTitleStyleDefault = 0, //图片在左,文字在右,整体居中。 - ButtonImageTitleStyleLeft = 1, //图片在左,文字在右,整体居中。 - ButtonImageTitleStyleRight = 2, //图片在右,文字在左,整体居中。 - ButtonImageTitleStyleTop = 3, //图片在上,文字在下,整体居中。 - ButtonImageTitleStyleBottom = 4, //图片在下,文字在上,整体居中。 - ButtonImageTitleStyleCenterTop = 5, //图片居中,文字在上距离按钮顶部。 - ButtonImageTitleStyleCenterBottom = 6, //图片居中,文字在下距离按钮底部。 - ButtonImageTitleStyleCenterUp = 7, //图片居中,文字在图片上面。 - ButtonImageTitleStyleCenterDown = 8, //图片居中,文字在图片下面。 - ButtonImageTitleStyleRightLeft = 9, //图片在右,文字在左,距离按钮两边边距 - ButtonImageTitleStyleLeftRight = 10, //图片在左,文字在右,距离按钮两边边距 -}; - -@interface UIButton (EG) - -- (void)SetUpButtonImageTitleStyle:(ButtonImageTitleStyle)style padding:(CGFloat)padding; - -- (void)AddTapBlock:(void(^)(UIButton*btn))block; - -@end - -NS_ASSUME_NONNULL_END diff --git a/crush/Crush/Src/Utils/Extensions/UIButton+EG.m b/crush/Crush/Src/Utils/Extensions/UIButton+EG.m deleted file mode 100644 index 1ec28b1..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIButton+EG.m +++ /dev/null @@ -1,209 +0,0 @@ -// -// UIButton+EG.m -// EGCommon -// -// Created by donglyu on 2020/4/9. -// Copyright © 2020 Company. All rights reserved. -// - -#import "UIButton+EG.h" -#import - -@implementation UIButton (EG) - -- (void)SetUpButtonImageTitleStyle:(ButtonImageTitleStyle)style padding:(CGFloat)padding{ - if (self.imageView.image != nil && self.titleLabel.text != nil) - { - - //先还原 - self.titleEdgeInsets = UIEdgeInsetsZero; - self.imageEdgeInsets = UIEdgeInsetsZero; - - CGRect imageRect = self.imageView.frame; - CGRect titleRect = self.titleLabel.frame; - - CGFloat totalHeight = imageRect.size.height + padding + titleRect.size.height; - CGFloat selfHeight = self.frame.size.height; - CGFloat selfWidth = self.frame.size.width; - - switch (style) { - case ButtonImageTitleStyleLeft: - if (padding != 0) - { - self.titleEdgeInsets = UIEdgeInsetsMake(0, - padding/2, - 0, - -padding/2); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - -padding/2, - 0, - padding/2); - } - break; - case ButtonImageTitleStyleRight: - { - //图片在右,文字在左 - self.titleEdgeInsets = UIEdgeInsetsMake(0, - -(imageRect.size.width + padding/2), - 0, - (imageRect.size.width + padding/2)); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - (titleRect.size.width+ padding/2), - 0, - -(titleRect.size.width+ padding/2)); - } - break; - case ButtonImageTitleStyleTop: - { - //图片在上,文字在下 - self.titleEdgeInsets = UIEdgeInsetsMake(((selfHeight - totalHeight)/2 + imageRect.size.height + padding - titleRect.origin.y), - (selfWidth/2 - titleRect.origin.x - titleRect.size.width /2) - (selfWidth - titleRect.size.width) / 2, - -((selfHeight - totalHeight)/2 + imageRect.size.height + padding - titleRect.origin.y), - -(selfWidth/2 - titleRect.origin.x - titleRect.size.width /2) - (selfWidth - titleRect.size.width) / 2); - - self.imageEdgeInsets = UIEdgeInsetsMake(((selfHeight - totalHeight)/2 - imageRect.origin.y), - (selfWidth /2 - imageRect.origin.x - imageRect.size.width / 2), - -((selfHeight - totalHeight)/2 - imageRect.origin.y), - -(selfWidth /2 - imageRect.origin.x - imageRect.size.width / 2)); - - } - break; - case ButtonImageTitleStyleBottom: - { - //图片在下,文字在上。 - self.titleEdgeInsets = UIEdgeInsetsMake(((selfHeight - totalHeight)/2 - titleRect.origin.y), - (selfWidth/2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2, - -((selfHeight - totalHeight)/2 - titleRect.origin.y), - -(selfWidth/2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2); - - self.imageEdgeInsets = UIEdgeInsetsMake(((selfHeight - totalHeight)/2 + titleRect.size.height + padding - imageRect.origin.y), - (selfWidth /2 - imageRect.origin.x - imageRect.size.width / 2), - -((selfHeight - totalHeight)/2 + titleRect.size.height + padding - imageRect.origin.y), - -(selfWidth /2 - imageRect.origin.x - imageRect.size.width / 2)); - } - break; - case ButtonImageTitleStyleCenterTop: - { - self.titleEdgeInsets = UIEdgeInsetsMake(-(titleRect.origin.y - padding), - (selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2, - (titleRect.origin.y - padding), - -(selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - (selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2), - 0, - -(selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2)); - } - break; - case ButtonImageTitleStyleCenterBottom: - { - self.titleEdgeInsets = UIEdgeInsetsMake((selfHeight - padding - titleRect.origin.y - titleRect.size.height), - (selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2, - -(selfHeight - padding - titleRect.origin.y - titleRect.size.height), - -(selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - (selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2), - 0, - -(selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2)); - } - break; - case ButtonImageTitleStyleCenterUp: - { - self.titleEdgeInsets = UIEdgeInsetsMake(-(titleRect.origin.y + titleRect.size.height - imageRect.origin.y + padding), - (selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2, - (titleRect.origin.y + titleRect.size.height - imageRect.origin.y + padding), - -(selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - (selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2), - 0, - -(selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2)); - } - break; - case ButtonImageTitleStyleCenterDown: - { - self.titleEdgeInsets = UIEdgeInsetsMake((imageRect.origin.y + imageRect.size.height - titleRect.origin.y + padding), - (selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2, - -(imageRect.origin.y + imageRect.size.height - titleRect.origin.y + padding), - -(selfWidth / 2 - titleRect.origin.x - titleRect.size.width / 2) - (selfWidth - titleRect.size.width) / 2); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - (selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2), - 0, - -(selfWidth / 2 - imageRect.origin.x - imageRect.size.width / 2)); - } - break; - case ButtonImageTitleStyleRightLeft: - { - //图片在右,文字在左,距离按钮两边边距 - - self.titleEdgeInsets = UIEdgeInsetsMake(0, - -(titleRect.origin.x - padding), - 0, - (titleRect.origin.x - padding)); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - (selfWidth - padding - imageRect.origin.x - imageRect.size.width), - 0, - -(selfWidth - padding - imageRect.origin.x - imageRect.size.width)); - } - - break; - - case ButtonImageTitleStyleLeftRight: - { - //图片在左,文字在右,距离按钮两边边距 - - self.titleEdgeInsets = UIEdgeInsetsMake(0, - (selfWidth - padding - titleRect.origin.x - titleRect.size.width), - 0, - -(selfWidth - padding - titleRect.origin.x - titleRect.size.width)); - - self.imageEdgeInsets = UIEdgeInsetsMake(0, - -(imageRect.origin.x - padding), - 0, - (imageRect.origin.x - padding)); - - - - } - break; - default: - break; - } - } - else { - self.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 0); - self.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 0); - } - -} - -#pragma mark - objc runtime to block to handler click event. - -- (void)setBlock:(void(^)(UIButton*))block{ - objc_setAssociatedObject(self,@selector(block), block, OBJC_ASSOCIATION_COPY_NONATOMIC); - [self addTarget: self action:@selector(click:)forControlEvents:UIControlEventTouchUpInside]; -} - -- (void(^)(UIButton*))block{ - return objc_getAssociatedObject(self,@selector(block)); -} - -- (void)AddTapBlock:(void(^)(UIButton*btn))block{ - self.block= block; - [self addTarget:self action:@selector(click:)forControlEvents:UIControlEventTouchUpInside]; -} - -- (void)click:(UIButton*)btn{ - if(self.block) { - self.block(btn); - } - -} - - -@end diff --git a/crush/Crush/Src/Utils/Extensions/UIButtonExt.swift b/crush/Crush/Src/Utils/Extensions/UIButtonExt.swift deleted file mode 100644 index ba9b054..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIButtonExt.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// UIButtonExt.swift -// Crush -// -// Created by Leon on 2025/7/18. -// - -extension UIButton { - // 使用 associated object 存储点击范围的内边距 - private struct AssociatedKeys { - static var touchAreaInsets = "touchAreaInsets" - } - - // 可动态设置的点击范围内边距 - @objc var touchAreaInsets: UIEdgeInsets { - get { - return objc_getAssociatedObject(self, &AssociatedKeys.touchAreaInsets) as? UIEdgeInsets ?? .zero - } - set { - objc_setAssociatedObject(self, &AssociatedKeys.touchAreaInsets, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - // 重写 point(inside:with:) 方法,扩展点击范围 - override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - let insets = touchAreaInsets - // 扩展后的点击区域 - let extendedBounds = bounds.inset(by: UIEdgeInsets( - top: -insets.top, - left: -insets.left, - bottom: -insets.bottom, - right: -insets.right - )) - return extendedBounds.contains(point) - } -} - -// 全局防抖影响一些基本的barButton的点击事件,和一些三方库冲突 -//import ObjectiveC.runtime -// -//extension UIControl { -// private struct AssociatedKeys { -// static var ignoreEvent = 0 -// static var clickInterval = 0 -// } -// -// /// 是否正在忽略点击事件 -// private var isIgnoringEvent: Bool { -// get { (objc_getAssociatedObject(self, &AssociatedKeys.ignoreEvent) as? Bool) ?? false } -// set { objc_setAssociatedObject(self, &AssociatedKeys.ignoreEvent, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } -// } -// -// /// 点击间隔,默认 0.8 秒 -// var clickInterval: TimeInterval { -// get { (objc_getAssociatedObject(self, &AssociatedKeys.clickInterval) as? TimeInterval) ?? 0.8 } -// set { objc_setAssociatedObject(self, &AssociatedKeys.clickInterval, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } -// } -// -// // MARK: - 启用防抖(全局一次即可) -// public static func enableClickDebounce() { -// let originalSelector = #selector(UIControl.sendAction(_:to:for:)) -// let swizzledSelector = #selector(UIControl.swizzled_sendAction(_:to:for:)) -// -// guard let originalMethod = class_getInstanceMethod(UIControl.self, originalSelector), -// let swizzledMethod = class_getInstanceMethod(UIControl.self, swizzledSelector) else { -// return -// } -// method_exchangeImplementations(originalMethod, swizzledMethod) -// } -// -// @objc private func swizzled_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { -// if isIgnoringEvent { -// return -// } -// -// if clickInterval > 0 { -// isIgnoringEvent = true -// DispatchQueue.main.asyncAfter(deadline: .now() + clickInterval) { [weak self] in -// self?.isIgnoringEvent = false -// } -// } -// -// // 调用原始实现 -// swizzled_sendAction(action, to: target, for: event) -// } -//} diff --git a/crush/Crush/Src/Utils/Extensions/UIColorExt.swift b/crush/Crush/Src/Utils/Extensions/UIColorExt.swift deleted file mode 100755 index d53df1d..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIColorExt.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// UIColor+.swift -// Demo1 -// -// Created by lym on 2019/5/22. -// Copyright © 2019 lym. All rights reserved. -// - -import Foundation -import UIKit - -public extension UIColor { - convenience init(hex: String) { - let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) - var int: UInt64 = 0 - Scanner(string: hex).scanHexInt64(&int) - let a, r, g, b: UInt64 - switch hex.count { - case 3: // RGB (12-bit) - (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) - case 6: // RGB (24-bit) - (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) - case 8: // ARGB (32-bit) - (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) - default: - (a, r, g, b) = (255, 0, 0, 0) - } - self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) - } - - /// 随机颜色 - static var random: UIColor { - return UIColor(red: CGFloat(arc4random() % 256) / 255.0, - green: CGFloat(arc4random() % 256) / 255.0, - blue: CGFloat(arc4random() % 256) / 255.0, - alpha: 0.5) - } - - /// 根据16进制颜色值返回颜色 - /// - Parameters: - /// - hexString: 颜色值字符串: 前缀 ‘#’ 和 ‘0x’ 不是必须的 - /// - alpha: 透明度,默认为1 - /// - Returns: UIColor - @objc static func hexString(_ hexString: String, alpha: CGFloat = 1) -> UIColor { - var str = "" - if hexString.lowercased().hasPrefix("0x") { - str = hexString.replacingOccurrences(of: "0x", with: "") - } else if hexString.lowercased().hasPrefix("#") { - str = hexString.replacingOccurrences(of: "#", with: "") - } else { - str = hexString - } - - let length = str.count - // 如果不是 RGB RGBA RRGGBB RRGGBBAA 结构 - if length != 3 && length != 4 && length != 6 && length != 8 { - return .clear - } - - // 将 RGB RGBA 转换为 RRGGBB RRGGBBAA 结构 - if length < 5 { - var tStr = "" - str.forEach { tStr.append(String(repeating: $0, count: 2)) } - str = tStr - } - - guard let hexValue = Int(str, radix: 16) else { - return .clear - } - - var red = 0 - var green = 0 - var blue = 0 - - if length == 3 || length == 6 { - red = (hexValue >> 16) & 0xFF - green = (hexValue >> 8) & 0xFF - blue = hexValue & 0xFF - } else { - red = (hexValue >> 20) & 0xFF - green = (hexValue >> 16) & 0xFF - blue = (hexValue >> 8) & 0xFF - } - return UIColor(red: CGFloat(red) / 255.0, - green: CGFloat(green) / 255.0, - blue: CGFloat(blue) / 255.0, - alpha: CGFloat(alpha)) - } - - /// 根据16进制颜色值返回颜色 - /// - Parameters: - /// - hex: 16进制数值 - /// - alpha: 透明度,默认为1 - /// - Returns: UIColor - static func hexInt32(_ hex: Int32, alpha: CGFloat = 1) -> UIColor { - let r = CGFloat((hex & 0xFF0000) >> 16) / 255 - let g = CGFloat((hex & 0xFF00) >> 8) / 255 - let b = CGFloat(hex & 0xFF) / 255 - return UIColor(red: r, green: g, blue: b, alpha: alpha) - } - - /// 颜色 --> 图片 - /// - Returns: UIImage? - func toImage(size: CGSize = CGSize(width: 1, height: 1)) -> UIImage? { - let rect = CGRect(origin: .zero, size: size) - UIGraphicsBeginImageContext(size) - let context = UIGraphicsGetCurrentContext()! - context.setFillColor(cgColor) - context.fill(rect) - guard let image = UIGraphicsGetImageFromCurrentImageContext() else { - return nil - } - UIGraphicsEndImageContext() - return image - } - - /// 生成渐变背景图片 - static func toGradientImage(frame: CGRect, - startPoint: CGPoint, - endPoint: CGPoint, - locs: [NSNumber] = [0, 1], - colors: [Any]) -> UIImage? - { - guard startPoint.x >= 0, - startPoint.x <= 1, - startPoint.y >= 0, - startPoint.y <= 1, - endPoint.x >= 0, - endPoint.x <= 1, - endPoint.y >= 0, - endPoint.y <= 1, - frame.size.width > 0, - frame.size.height > 0 - else { - return nil - } - - let gradientLayer = CAGradientLayer() - gradientLayer.frame = frame - gradientLayer.startPoint = startPoint - gradientLayer.endPoint = endPoint - gradientLayer.colors = colors - gradientLayer.masksToBounds = true - gradientLayer.locations = locs - - UIGraphicsBeginImageContext(gradientLayer.bounds.size) - gradientLayer.render(in: UIGraphicsGetCurrentContext()!) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UIDeviceExt.swift b/crush/Crush/Src/Utils/Extensions/UIDeviceExt.swift deleted file mode 100755 index c83173d..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIDeviceExt.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// UIDevice+Ext.swift -// LegendTeam -// -// Created by lym on 2022/2/16. -// - -import Foundation -import UIKit -import UICKeyChainStore - -extension UIDevice { - static var UUID: String { - let service = Bundle.appBundleID + ".uuid" - let key = Bundle.appBundleID + ".uuid" + ".key" - let keychain = UICKeyChainStore(service: service) - if let uuid = keychain.string(forKey: key) { - return uuid - } - // 8-4-4-4-12 - // F9C5E3F8-2396-435E-AD11-02446C943FB2 - var uuid2 = UIDevice.current.identifierForVendor?.uuidString - if uuid2 == nil { - let timeInterval = Int(Date().timeIntervalSince1970) - uuid2 = String.randomString(8) - + "-" + String.randomString(4) - + "-" + String.randomString(4) - + "-" + String.randomString(4) - + "-" + "\(timeInterval)" + String.randomString(2) - } - keychain.setString(uuid2, forKey: key) - return uuid2! - } - - var isPhone: Bool { - return userInterfaceIdiom == .phone - } - - var isPad: Bool { - return userInterfaceIdiom == .pad - } - - var isSimulator: Bool { - #if targetEnvironment(simulator) - return true - #else - return false - #endif - } - - var hasNotch: Bool { - if #available(iOS 15.0, *) { - // Use UIWindowScene.windows for iOS 15 and later - let windowScene = UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene } - .first - return windowScene?.windows.first?.safeAreaInsets.bottom ?? 0 > 0 - } else { - // Fallback for iOS versions before 15.0 - return UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 > 0 - } - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UIImageExt.swift b/crush/Crush/Src/Utils/Extensions/UIImageExt.swift deleted file mode 100755 index bb0357a..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIImageExt.swift +++ /dev/null @@ -1,546 +0,0 @@ -// -// UIImage+Ext.swift -// LegendTeam -// -// Created by dong on 2021/12/15. -// - -import UIKit - -public extension UIImage { - class func withColor(color: UIColor, size: CGSize) -> UIImage { - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - let context = UIGraphicsGetCurrentContext() - context?.setFillColor(color.cgColor) - context?.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image! - } - - class func withColor(color: UIColor) -> UIImage { - return UIImage.withColor(color: color, size: CGSize(width: 1, height: 1)) - } - - static func gradientHImageWithSize(size: CGSize, colors: [UIColor]) -> UIImage { - let gradientLayer = CAGradientLayer() - gradientLayer.frame = CGRect(origin: .zero, size: size) - var cgColors : [CGColor] = [] - for per in colors { - cgColors.append(per.cgColor) - } - gradientLayer.colors = cgColors - gradientLayer.startPoint = CGPoint(x: 0, y: 1) - gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.5) - gradientLayer.locations = [0, 1] - UIGraphicsBeginImageContext(gradientLayer.bounds.size) - gradientLayer.render(in: UIGraphicsGetCurrentContext()!) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image! - } - - static func gradientVImageWithSize(size: CGSize, colors: [UIColor]) -> UIImage { - let gradientLayer = CAGradientLayer() - gradientLayer.frame = CGRect(origin: .zero, size: size) - var cgColors : [CGColor] = [] - for per in colors { - cgColors.append(per.cgColor) - } - gradientLayer.colors = cgColors - gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) - gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) - gradientLayer.locations = [0, 1] - UIGraphicsBeginImageContext(gradientLayer.bounds.size) - gradientLayer.render(in: UIGraphicsGetCurrentContext()!) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image! - } - - static func gradientImageWithSize(size: CGSize, colors: [CGColor], startPoint: CGPoint, endPoint: CGPoint) -> UIImage { - let gradientLayer = CAGradientLayer() - gradientLayer.frame = CGRect(origin: .zero, size: size) - gradientLayer.colors = colors - gradientLayer.startPoint = startPoint - gradientLayer.endPoint = endPoint - if(colors.count == 2){ - gradientLayer.locations = [0, 1] - }else if(colors.count == 3){ - gradientLayer.locations = [0, 0.5, 1] - } - - UIGraphicsBeginImageContextWithOptions(size, false, 0.0) - defer { UIGraphicsEndImageContext() } - guard let context = UIGraphicsGetCurrentContext() else { return UIImage() } - gradientLayer.render(in: context) - guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() } - - return image - } - - func compress(toByte maxLength: Int) -> UIImage { - var compression: CGFloat = 1 - guard var data = jpegData(compressionQuality: compression), - data.count > maxLength else { return self } - - dlog("压缩前:\(data.count / 1024) KB") - - // Compress by size - var max: CGFloat = 1 - var min: CGFloat = 0 - for _ in 0 ..< 6 { - compression = (max + min) / 2 - data = jpegData(compressionQuality: compression)! - if CGFloat(data.count) < CGFloat(maxLength) * 0.9 { - min = compression - } else if data.count > maxLength { - max = compression - } else { - break - } - } - dlog("压缩(by size)后1:\(data.count / 1024) KB") - - var resultImage = UIImage(data: data)! - if data.count < maxLength { return resultImage } - - // Compress by size - var lastDataLength: Int = 0 - while data.count > maxLength, data.count != lastDataLength { - lastDataLength = data.count - let ratio = CGFloat(maxLength) / CGFloat(data.count) - let size = CGSize(width: Int(resultImage.size.width * sqrt(ratio)), - height: Int(resultImage.size.height * sqrt(ratio))) - UIGraphicsBeginImageContext(size) - resultImage.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) - resultImage = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() - data = resultImage.jpegData(compressionQuality: compression)! - } - dlog("压缩(by size)后2:\(data.count / 1024) KB") - return resultImage - } - - static func circleImage(withColor color: UIColor, bounds: CGRect) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale) - guard let context = UIGraphicsGetCurrentContext() else { return nil } - - context.setStrokeColor(UIColor.clear.cgColor) - context.addEllipse(in: bounds) - context.clip() - context.setFillColor(color.cgColor) - context.fill(bounds) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - return image - } -} - -// properties -public extension UIImage { - /// Size in bytes of UIImage - var bytesSize: Int { - return jpegData(compressionQuality: 1)?.count ?? 0 - } - - /// Size in kilo bytes of UIImage - var kilobytesSize: Int { - return (jpegData(compressionQuality: 1)?.count ?? 0) / 1024 - } - - /// UIImage with .alwaysOriginal rendering mode. - var original: UIImage { - return withRenderingMode(.alwaysOriginal) - } - - /// UIImage with .alwaysTemplate rendering mode. - var template: UIImage { - return withRenderingMode(.alwaysTemplate) - } - - var pixelSize:CGSize{ - return CGSize(width: size.width*scale, height: size.height*scale) - } - - /// Jpeg: default: 0.8 - func imageToBase64(_ image: UIImage, format: ImageFormat = .jpeg(0.8)) -> String? { - var imageData: Data? - - switch format { - case .png: - imageData = image.pngData() - case .jpeg(let quality): - imageData = image.jpegData(compressionQuality: quality) - } - - return imageData?.base64EncodedString() - } - - enum ImageFormat { - case png - case jpeg(CGFloat) - } -} - -// methods -public extension UIImage { - /// Compressed UIImage from original UIImage. - /// - /// - Parameter quality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality), (default is 0.5). - /// - Returns: optional UIImage (if applicable). - func compressed(quality: CGFloat = 0.5) -> UIImage? { - guard let data = jpegData(compressionQuality: quality) else { return nil } - return UIImage(data: data) - } - - /// Compressed UIImage data from original UIImage. - /// - /// - Parameter quality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality), (default is 0.5). - /// - Returns: optional Data (if applicable). - func compressedData(quality: CGFloat = 0.5) -> Data? { - return jpegData(compressionQuality: quality) - } - - /// UIImage Cropped to CGRect. - /// - /// - Parameter rect: CGRect to crop UIImage to. - /// - Returns: cropped UIImage - func cropped(to rect: CGRect) -> UIImage { - guard rect.size.width <= size.width && rect.size.height <= size.height else { return self } - guard let image: CGImage = cgImage?.cropping(to: rect) else { return self } - return UIImage(cgImage: image) - } - - func cropImageTop(with scale: CGFloat) -> UIImage? { - // Original image - guard let image = self as UIImage? else { return nil } - - // Get CGImage - guard let imageRef = image.cgImage else { return nil } - - // Calculate height - var height = image.size.width * scale - if height > image.size.height { - height = image.size.height - } - - // Define crop area (full width, dynamic height from top) - let cutArea = CGRect(x: 0, y: 0, width: image.size.width, height: height) - - // Crop the image - guard let cgImage = imageRef.cropping(to: cutArea) else { return nil } - - // Create new UIImage from cropped CGImage - let cutImage = UIImage(cgImage: cgImage) - - return cutImage - } - - /// UIImage scaled to height with respect to aspect ratio. - /// - /// - Parameters: - /// - toHeight: new height. - /// - opaque: flag indicating whether the bitmap is opaque. - /// - Returns: optional scaled UIImage (if applicable). - func scaled(toHeight: CGFloat, opaque: Bool = false) -> UIImage? { - let scale = toHeight / size.height - let newWidth = size.width * scale - UIGraphicsBeginImageContextWithOptions(CGSize(width: newWidth, height: toHeight), opaque, UIScreen.main.scale) // - draw(in: CGRect(x: 0, y: 0, width: newWidth, height: toHeight)) - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return newImage - } - - /// UIImage scaled to width with respect to aspect ratio. - /// - /// - Parameters: - /// - toWidth: new width. - /// - opaque: flag indicating whether the bitmap is opaque. - /// - Returns: optional scaled UIImage (if applicable). - func scaled(toWidth: CGFloat, opaque: Bool = false) -> UIImage? { - let scale = toWidth / size.width - let newHeight = size.height * scale - UIGraphicsBeginImageContextWithOptions(CGSize(width: toWidth, height: newHeight), opaque, self.scale) - draw(in: CGRect(x: 0, y: 0, width: toWidth, height: newHeight)) - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return newImage - } - - /// Creates a copy of the receiver rotated by the given angle. - /// - /// // Rotate the image by 180° - /// image.rotated(by: Measurement(value: 180, unit: .degrees)) - /// - /// - Parameter angle: The angle measurement by which to rotate the image. - /// - Returns: A new image rotated by the given angle. - @available(iOS 10.0, tvOS 10.0, watchOS 3.0, *) - func rotated(by angle: Measurement) -> UIImage? { - let radians = CGFloat(angle.converted(to: .radians).value) - - let destRect = CGRect(origin: .zero, size: size) - .applying(CGAffineTransform(rotationAngle: radians)) - let roundedDestRect = CGRect(x: destRect.origin.x.rounded(), - y: destRect.origin.y.rounded(), - width: destRect.width.rounded(), - height: destRect.height.rounded()) - - UIGraphicsBeginImageContext(roundedDestRect.size) - guard let contextRef = UIGraphicsGetCurrentContext() else { return nil } - - contextRef.translateBy(x: roundedDestRect.width / 2, y: roundedDestRect.height / 2) - contextRef.rotate(by: radians) - - draw(in: CGRect(origin: CGPoint(x: -size.width / 2, - y: -size.height / 2), - size: size)) - - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return newImage - } - - /// Creates a copy of the receiver rotated by the given angle (in radians). - /// - /// // Rotate the image by 180° - /// image.rotated(by: .pi) - /// - /// - Parameter radians: The angle, in radians, by which to rotate the image. - /// - Returns: A new image rotated by the given angle. - func rotated(by radians: CGFloat) -> UIImage? { - let destRect = CGRect(origin: .zero, size: size) - .applying(CGAffineTransform(rotationAngle: radians)) - let roundedDestRect = CGRect(x: destRect.origin.x.rounded(), - y: destRect.origin.y.rounded(), - width: destRect.width.rounded(), - height: destRect.height.rounded()) - - UIGraphicsBeginImageContext(roundedDestRect.size) - guard let contextRef = UIGraphicsGetCurrentContext() else { return nil } - - contextRef.translateBy(x: roundedDestRect.width / 2, y: roundedDestRect.height / 2) - contextRef.rotate(by: radians) - - draw(in: CGRect(origin: CGPoint(x: -size.width / 2, - y: -size.height / 2), - size: size)) - - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return newImage - } - - /// UIImage filled with color - /// - /// - Parameter color: color to fill image with. - /// - Returns: UIImage filled with given color. - func filled(withColor color: UIColor) -> UIImage { - #if !os(watchOS) - if #available(iOS 10, tvOS 10, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = scale - let renderer = UIGraphicsImageRenderer(size: size, format: format) - return renderer.image { context in - color.setFill() - context.fill(CGRect(origin: .zero, size: size)) - } - } - #endif - - UIGraphicsBeginImageContextWithOptions(size, false, scale) - color.setFill() - guard let context = UIGraphicsGetCurrentContext() else { return self } - - context.translateBy(x: 0, y: size.height) - context.scaleBy(x: 1.0, y: -1.0) - context.setBlendMode(CGBlendMode.normal) - - let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) - guard let mask = cgImage else { return self } - context.clip(to: rect, mask: mask) - context.fill(rect) - - let newImage = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() - return newImage - } - - /// UIImage tinted with color - /// - /// - Parameters: - /// - color: color to tint image with. - /// - blendMode: how to blend the tint - /// - Returns: UIImage tinted with given color. - func tint(_ color: UIColor, blendMode: CGBlendMode, alpha: CGFloat = 1.0) -> UIImage { - let drawRect = CGRect(origin: .zero, size: size) - - #if !os(watchOS) - if #available(iOS 10.0, tvOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = scale - return UIGraphicsImageRenderer(size: size, format: format).image { context in - color.setFill() - context.fill(drawRect) - draw(in: drawRect, blendMode: blendMode, alpha: alpha) - } - } - #endif - - UIGraphicsBeginImageContextWithOptions(size, false, scale) - defer { - UIGraphicsEndImageContext() - } - let context = UIGraphicsGetCurrentContext() - color.setFill() - context?.fill(drawRect) - draw(in: drawRect, blendMode: blendMode, alpha: alpha) - return UIGraphicsGetImageFromCurrentImageContext()! - } - - /// UImage with background color - /// - /// - Parameters: - /// - backgroundColor: Color to use as background color - /// - Returns: UIImage with a background color that is visible where alpha < 1 - func withBackgroundColor(_ backgroundColor: UIColor) -> UIImage { - #if !os(watchOS) - if #available(iOS 10.0, tvOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = scale - return UIGraphicsImageRenderer(size: size, format: format).image { context in - backgroundColor.setFill() - context.fill(context.format.bounds) - draw(at: .zero) - } - } - #endif - - UIGraphicsBeginImageContextWithOptions(size, false, scale) - defer { UIGraphicsEndImageContext() } - - backgroundColor.setFill() - UIRectFill(CGRect(origin: .zero, size: size)) - draw(at: .zero) - - return UIGraphicsGetImageFromCurrentImageContext()! - } - - /// UIImage with rounded corners - /// - /// - Parameters: - /// - radius: corner radius (optional), resulting image will be round if unspecified - /// - Returns: UIImage with all corners rounded - func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? { - let maxRadius = min(size.width, size.height) / 2 - let cornerRadius: CGFloat - if let radius = radius, radius > 0 && radius <= maxRadius { - cornerRadius = radius - } else { - cornerRadius = maxRadius - } - - UIGraphicsBeginImageContextWithOptions(size, false, scale) - - let rect = CGRect(origin: .zero, size: size) - UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip() - draw(in: rect) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image - } - - /// Base 64 encoded PNG data of the image. - /// - /// - returns: Base 64 encoded PNG data of the image as a String. - func pngBase64String() -> String? { - return pngData()?.base64EncodedString() - } - - /// Base 64 encoded JPEG data of the image. - /// - /// - parameter compressionQuality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality). - /// - returns: Base 64 encoded JPEG data of the image as a String. - func jpegBase64String(compressionQuality: CGFloat) -> String? { - return jpegData(compressionQuality: compressionQuality)?.base64EncodedString() - } - - /// 创建一个可拉伸的UIImage,指定水平和垂直拉伸位置 - /// - Parameters: - /// - horizontalRatio: 水平拉伸点比例(0.0到1.0),表示拉伸点的相对位置 - /// - verticalRatio: 垂直拉伸点比例(0.0到1.0),表示拉伸点的相对位置 - /// - Returns: 可拉伸的UIImage - func stretchableImage(horizontalRatio: CGFloat, verticalRatio: CGFloat) -> UIImage { - // 确保比例在0.0到1.0之间 - let clampedHorizontalRatio = max(0.0, min(1.0, horizontalRatio)) - let clampedVerticalRatio = max(0.0, min(1.0, verticalRatio)) - - // 计算拉伸区域 - let width = self.size.width - let height = self.size.height - - let leftCap = floor(width * clampedHorizontalRatio) - let topCap = floor(height * clampedVerticalRatio) - - // 创建可拉伸图片 - return self.resizableImage(withCapInsets: UIEdgeInsets( - top: topCap, - left: leftCap, - bottom: height - topCap - 1, - right: width - leftCap - 1 - ), resizingMode: .stretch) - } -} - -public extension UIImage { - /// Create UIImage from color and size. - /// - /// - Parameters: - /// - color: image fill color. - /// - size: image size. - convenience init(color: UIColor, size: CGSize) { - UIGraphicsBeginImageContextWithOptions(size, false, 1) - - defer { - UIGraphicsEndImageContext() - } - - color.setFill() - UIRectFill(CGRect(origin: .zero, size: size)) - - guard let aCgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { - self.init() - return - } - - self.init(cgImage: aCgImage) - } - - /// Create a new image from a base 64 string. - /// - /// - Parameters: - /// - base64String: a base-64 `String`, representing the image - /// - scale: The scale factor to assume when interpreting the image data created from the base-64 string. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. - convenience init?(base64String: String, scale: CGFloat = 1.0) { - guard let data = Data(base64Encoded: base64String) else { return nil } - self.init(data: data, scale: scale) - } - - /// Create a new image from a URL - /// - /// - Important: - /// Use this method to convert data:// URLs to UIImage objects. - /// Don't use this synchronous initializer to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated. - /// Instead, for non-file URLs, consider using this in an asynchronous way, using `dataTask(with:completionHandler:)` method of the URLSession class or a library such as `AlamofireImage`, `Kingfisher`, `SDWebImage`, or others to perform asynchronous network image loading. - /// - Parameters: - /// - url: a `URL`, representing the image location - /// - scale: The scale factor to assume when interpreting the image data created from the URL. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. - convenience init?(url: URL, scale: CGFloat = 1.0) throws { - let data = try Data(contentsOf: url) - self.init(data: data, scale: scale) - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UIImageViewExt.swift b/crush/Crush/Src/Utils/Extensions/UIImageViewExt.swift deleted file mode 100755 index 3ffb307..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIImageViewExt.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// UIImageView+Ext.swift -// E-Wow -// -// Created by lym on 2021/1/8. -// - -import Foundation -import Kingfisher -import UIKit - -extension UIImageView -{ - private static let defaultOptions: KingfisherOptionsInfo = [ - .transition(.none), - .loadDiskFileSynchronously, - ] - - /** - 无s3逻辑 - 默认给图片设置bgColor(csbn) - */ - func loadImage(_ url: String?, - placeholder: Placeholder? = nil, - bgColor: UIColor? = .c.csbn, - options: KingfisherOptionsInfo? = defaultOptions) - { - backgroundColor = bgColor - kf.setImage(with: URL(string: url ?? ""), placeholder: placeholder, options: options) - } - - /// - Parameters: - /// - urlString: 图片的URL字符串 - /// - placeholder: 占位图(可选) - /// - progressBlock: 进度回调,接收已下载字节和总字节 - /// - completionBlock: 完成回调,接收加载结果 - func loadImage( - _ urlString: String?, - placeholder: Placeholder? = nil, - bgColor: UIColor? = .hexInt32(0xFBDEFF).withAlphaComponent(0.08), - progressBlock: ((_ receivedSize: Int64, _ totalSize: Int64) -> Void)? = nil, - completionBlock: ((Result) -> Void)? = nil - ) { - guard let urlString = urlString, let url = URL(string: urlString) else { - completionBlock?(.failure(.requestError(reason: .emptyRequest))) - return - } - - kf.setImage( - with: url, - placeholder: placeholder, - options: nil, - progressBlock: { receivedSize, totalSize in - // 进度回调 - progressBlock?(receivedSize, totalSize) - }, - completionHandler: { result in - // 完成回调 - completionBlock?(result) - } - ) - } - - -} - -struct ImageDownloader { - /// 使用 Kingfisher 下载图片,带 completion 回调 -// static func downloadImage( -// from url: URL?, -// completion: @escaping (UIImage?) -> Void -// ) { -// guard let url = url else { -// completion(nil) -// return -// } -// -// KingfisherManager.shared.retrieveImage(with: url) { result in -// switch result { -// case .success(let value): -// completion(value.image) // 成功 -// case .failure(let error): -// print("❌ 图片下载失败: \(error)") -// completion(nil) -// } -// } -// } - - /// 使用 Kingfisher 下载图片,传入可选 String 链接 - static func downloadImage( - from urlString: String?, - completion: @escaping (UIImage?) -> Void - ) { - guard - let urlString = urlString, - let url = URL(string: urlString) - else { - completion(nil) - return - } - - KingfisherManager.shared.retrieveImage(with: url) { result in - switch result { - case .success(let value): - completion(value.image) // 成功返回 UIImage - case .failure(let error): - print("❌ 图片下载失败: \(error)") - completion(nil) - } - } - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UINavigationControllerExt.swift b/crush/Crush/Src/Utils/Extensions/UINavigationControllerExt.swift deleted file mode 100755 index 66782c0..0000000 --- a/crush/Crush/Src/Utils/Extensions/UINavigationControllerExt.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// UINavigationController+Ext.swift.swift -// LegendTeam -// -// Created by dong on 2021/12/27. -// - -import Foundation -import UIKit - -extension UINavigationController { - @discardableResult - public func popToViewControllerType(type: AnyClass) -> UIViewController? { - let vcs = viewControllers.reversed() - for vc in vcs { - if vc.isKind(of: type) { - popToViewController(vc, animated: true) - return vc - } - } - return nil - } - - override open var childForStatusBarStyle: UIViewController? { - return viewControllers.last - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UITableViewExt.swift b/crush/Crush/Src/Utils/Extensions/UITableViewExt.swift deleted file mode 100755 index f0e404a..0000000 --- a/crush/Crush/Src/Utils/Extensions/UITableViewExt.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// UITableView+Ext.swift -// DouYinSwift5 -// -// Created by lym on 2021/6/8. -// Copyright © 2021 lym. All rights reserved. -// - -import UIKit - -extension UITableView { - /// Set table header view & add Auto layout. - func setTableHeaderView(headerView: UIView) { - headerView.translatesAutoresizingMaskIntoConstraints = false - - // Set first. - tableHeaderView = headerView - - // Then setup AutoLayout. - headerView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - headerView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true - headerView.topAnchor.constraint(equalTo: topAnchor).isActive = true - - updateHeaderViewFrame() - } - - /// Update header view's frame. - func updateHeaderViewFrame() { - guard let headerView = tableHeaderView else { return } - - // Update the size of the header based on its internal content. - headerView.layoutIfNeeded() - - // ***Trigger table view to know that header should be updated. - let header = tableHeaderView - tableHeaderView = header - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UITextFieldExt.swift b/crush/Crush/Src/Utils/Extensions/UITextFieldExt.swift deleted file mode 100755 index 601445b..0000000 --- a/crush/Crush/Src/Utils/Extensions/UITextFieldExt.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// UITextField+Ext.swift -// LegendTeam -// -// Created by dong on 2021/12/15. -// - -import UIKit - -extension UITextField{ - - func setupPhonefieldMode(){ - keyboardType = .phonePad - } - -} diff --git a/crush/Crush/Src/Utils/Extensions/UIViewExt.swift b/crush/Crush/Src/Utils/Extensions/UIViewExt.swift deleted file mode 100755 index c3f35e0..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIViewExt.swift +++ /dev/null @@ -1,311 +0,0 @@ -// -// UIView+Ext.swift -// DouYinSwift5 -// -// Created by lym on 2020/7/23. -// Copyright © 2020 lym. All rights reserved. -// - -import UIKit - - -public func adaptWidth(designWidth: CGFloat = 375.0, _ vale: CGFloat) -> CGFloat { - return UIScreen.main.bounds.size.width / designWidth * vale -} - -// MARK: - 属性 - -@objc extension UIView { - /// 边框颜色 - @IBInspectable var borderColor: UIColor? { - get { - guard let color = layer.borderColor else { return nil } - return UIColor(cgColor: color) - } - set { - guard let color = newValue else { - layer.borderColor = nil - return - } - layer.borderColor = color.cgColor - } - } - - /// 边框宽度 - @IBInspectable var borderWidth: CGFloat { - get { return layer.borderWidth } - set { layer.borderWidth = newValue } - } - - /// 圆角半径 - @IBInspectable var cornerRadius: CGFloat { - get { return layer.cornerRadius } - set { - layer.cornerRadius = newValue - layer.masksToBounds = true - } - } - - var size: CGSize { - get { return frame.size } - set { width = newValue.width; height = newValue.height } - } - - var width: CGFloat { - get { return frame.size.width } - set { return frame.size.width = newValue } - } - - var height: CGFloat { - get { return frame.size.height } - set { return frame.size.height = newValue } - } - - var x: CGFloat { - get { return frame.origin.x } - set { frame.origin.x = newValue } - } - - var y: CGFloat { - get { return frame.origin.y } - set { frame.origin.y = newValue } - } - - var centerX: CGFloat { - get { return center.x } - set { center.x = newValue } - } - - var centerY: CGFloat { - get { return center.y } - set { center.y = newValue } - } - - var origin: CGPoint { - get { return frame.origin } - set { frame.origin = newValue } - } -} - -@objc public extension UIView { - - - /// 删除所有的子视图 - func removeSubviews() { - subviews.forEach { $0.removeFromSuperview() } - } - - /// 添加阴影 - /// - Parameters: - /// - color: 颜色 - /// - radius: 半径 - /// - offset: 偏移 - /// - opacity: 透明度 - func addShadow(ofColor color: UIColor = UIColor(red: 0.07, green: 0.47, blue: 0.57, alpha: 1.0), - radius: CGFloat = 3, - offset: CGSize = .zero, - opacity: Float = 0.5) - { - layer.shadowColor = color.cgColor - layer.shadowOffset = offset - layer.shadowRadius = radius - layer.shadowOpacity = opacity - layer.masksToBounds = false - } - - /// 返回视图的控制器对象 - /// - /// - Returns: 控制器对象,可能为空 - func viewController() -> UIViewController? { - var view: UIView? = self - repeat { - if let nextResponder = view?.next { - if nextResponder.isKind(of: UIViewController.self) { - return nextResponder as? UIViewController - } - } - view = view?.superview - } while view != nil - - return nil - } -} - -@objc extension UIView { - @discardableResult - /// 添加渐变背景 - func addGradientColor(startPoint: CGPoint, - endPoint: CGPoint, - locs: [NSNumber] = [0, 1], - colors: [Any], - cornerRadius: CGFloat = 0) -> CAGradientLayer? - { - guard startPoint.x >= 0, - startPoint.x <= 1, - startPoint.y >= 0, - startPoint.y <= 1, - endPoint.x >= 0, - endPoint.x <= 1, - endPoint.y >= 0, - endPoint.y <= 1 - else { - return nil - } - - layoutIfNeeded() - - var gradientLayer: CAGradientLayer! - - removeGradientLayer() - - gradientLayer = CAGradientLayer() - gradientLayer.frame = layer.bounds - gradientLayer.startPoint = startPoint - gradientLayer.endPoint = endPoint - gradientLayer.colors = colors - gradientLayer.cornerRadius = layer.cornerRadius - gradientLayer.masksToBounds = true - gradientLayer.locations = locs - - if cornerRadius > 0 { - let shapeLayer = CAShapeLayer() - shapeLayer.borderWidth = 1 - shapeLayer.path = UIBezierPath(roundedRect: gradientLayer.bounds, cornerRadius: cornerRadius).cgPath - shapeLayer.fillColor = UIColor.clear.cgColor // 必须要设置成clearColor或者nil,默认是黑色 - shapeLayer.strokeColor = UIColor.white.cgColor // 随便设置一个非clearColor的颜色 - gradientLayer.mask = shapeLayer - } - - // 渐变图层插入到最底层,避免在uibutton上遮盖文字图片 - layer.insertSublayer(gradientLayer, at: 0) - backgroundColor = UIColor.clear - // self如果是UILabel,masksToBounds设为true会导致文字消失 - // layer.masksToBounds = false - return gradientLayer - } - - public func removeGradientLayer() { - if let sl = layer.sublayers { - for layer in sl { - if layer.isKind(of: CAGradientLayer.self) { - layer.removeFromSuperlayer() - } - } - } - } - - func addDashedBorder(to view: UIView, lineLength: CGFloat = 8, lineSpacing: CGFloat = 2, cornerRadius: CGFloat = 0) { - let shapeLayer = CAShapeLayer() - shapeLayer.name = "DashedBorder" - let lineColor = UIColor.c.con - // let lineColor = UIColor.red - - shapeLayer.bounds = view.bounds - shapeLayer.position = CGPoint(x: view.bounds.width / 2, y: view.bounds.height / 2) - shapeLayer.fillColor = UIColor.clear.cgColor - shapeLayer.strokeColor = lineColor.cgColor - shapeLayer.lineWidth = 1 - shapeLayer.lineJoin = .round - shapeLayer.lineDashPattern = [NSNumber(value: Float(lineLength)), NSNumber(value: Float(lineSpacing))] - - let path = UIBezierPath(roundedRect: view.bounds, cornerRadius: cornerRadius) - shapeLayer.path = path.cgPath - - // 先移除旧的虚线层(避免重复添加) - view.layer.sublayers?.removeAll(where: { $0.name == "DashedBorder" }) - - view.layer.addSublayer(shapeLayer) - } - -} - -extension UIView{ - /// 将一个矩形区域从指定视图坐标系转换到当前视图或窗口坐标系 - /// - /// 使用方法: - /// ``` - /// CGRect rect = [self.view convertRect:_button.frame fromViewOrWindow:_button.superview]; - /// ``` - /// - /// button的frame是相对于其superview来确定的,frame确定了button在其superview的位置和大小 - /// - /// 一般来说,toView方法中,消息的接收者为被转换的frame所在的控件的superview;fromView方法中,消息的接收者为即将转到的目标view. - /// - /// - Parameters: - /// - rect: 矩形区域 - /// - view: 指定视图或窗口 - /// - Returns: 当前坐标系的矩形区域 - func convertRect(rect: CGRect, fromViewOrWindow view: UIView?) -> CGRect { - guard let view = view else { - if isKind(of: UIWindow.self) { - return (self as! UIWindow).convert(rect, from: nil) - } else { - return convert(rect, from: nil) - } - } - - if let from = view.isKind(of: UIWindow.self) ? (view as! UIWindow) : view.window, - let to = isKind(of: UIWindow.self) ? (self as! UIWindow) : window, from != to { - var r = rect - r = from.convert(r, from: view) - r = to.convert(r, from: from) - r = convert(r, from: to) - return r - } else { - return convert(rect, from: view) - } - } -} - -extension UIView{ - /// 扩展UIView增加抖动方法 - /// - /// - Parameters: - /// - isVertical: 抖动方向(默认是水平方向) - /// - times: 抖动次数(默认5次) - /// - interval: 每次抖动时间(默认0.1秒) - /// - delta: 抖动偏移量(默认2) - /// - completion: 抖动动画结束后的回调 - public func shake(_ isVertical: Bool = false, - _ times: Int = 5, - _ interval: TimeInterval = 0.1, - _ delta: CGFloat = 2, - _ completion: (() -> ())? = nil) { - // 播放动画 - UIView.animate(withDuration: interval) { - var transform = CGAffineTransform(translationX: 0, y: delta) - if isVertical == true { - transform = CGAffineTransform(translationX: delta, y: 0) - } - self.layer.setAffineTransform(transform) - } completion: {_ in - // 如果当前是最后一次抖动,则将位置还原,并调用完成回调函数 - if (times == 0) { - UIView.animate(withDuration: interval) { - self.layer.setAffineTransform(CGAffineTransform.identity) - } completion: { _ in - completion?() - } - }else { - // 如果当前不是最后一次抖动,则继续播放动画(总次数减1,偏移位置变成相反的) - self.shake(isVertical, times - 1, interval, -delta, completion) - } - } - } -} - -extension UIView { - //将当前视图转为UIImage - func asImage() -> UIImage { - let renderer = UIGraphicsImageRenderer(bounds: bounds) - return renderer.image { rendererContext in - layer.render(in: rendererContext.cgContext) - } - } - - /// 获取视图在屏幕上的绝对坐标(相对于整个屏幕) - var screenRect: CGRect? { - guard let window = self.window else { return nil } - return self.superview?.convert(self.frame, to: window) - } -} diff --git a/crush/Crush/Src/Utils/Extensions/UIWindowExt.swift b/crush/Crush/Src/Utils/Extensions/UIWindowExt.swift deleted file mode 100755 index a4c6826..0000000 --- a/crush/Crush/Src/Utils/Extensions/UIWindowExt.swift +++ /dev/null @@ -1,251 +0,0 @@ -// -// UIWindow+Ext.swift -// DouYinSwift5 -// -// Created by lym on 2020/7/23. -// Copyright © 2020 lym. All rights reserved. -// -import UIKit - -@objc extension UIWindow { - static var key: UIWindow? { - if #available(iOS 13, *) { - return UIApplication.shared.connectedScenes - .map { $0 as? UIWindowScene } - .compactMap { $0 } - .first?.windows - .filter { $0.isKeyWindow }.first - } else { - return UIApplication.shared.windows - .filter { $0.isKeyWindow } - .first - } - } - - static var applicationKey: UIWindow? { - if #available(iOS 13, *) { - var window: UIWindow? - let connectedScenes = UIApplication.shared.connectedScenes - .map { $0 as? UIWindowScene } - .compactMap { $0 } - connectedScenes.forEach { scene in - window = scene.windows.filter { $0.tag == 1024 }.first - } - return window - } else { - return UIApplication.shared.windows.first(where: { $0.tag == 1024 }) - } - } - - /// 获取当前激活的 SceneDelegate - static var currentSceneDelegate: SceneDelegate? { - if let scene = UIApplication.shared.connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - return scene.delegate as? SceneDelegate - } - return nil - } - - /// 获取当前激活窗口 - static var current: UIWindow? { - if let scene = UIApplication.shared.connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - return scene.windows.first(where: { $0.isKeyWindow }) - } - return nil - } - - static var safeAreaInsets: UIEdgeInsets { - return UIWindow.key?.safeAreaInsets ?? UIEdgeInsets.zero - } - - static var safeAreaBottom: CGFloat { - return safeAreaInsets.bottom - } - - static var statusBarFrame: CGRect { - if #available(iOS 13.0, *) { - return key?.windowScene?.statusBarManager?.statusBarFrame ?? .zero - } else { - return UIApplication.shared.statusBarFrame - } - } - - static var statusBarHeight: CGFloat { - return statusBarFrame.size.height - } - - static var navBarHeight: CGFloat { - return getTopViewController()?.navigationController?.navigationBar.frame.height ?? 44 - } - - static var navBarTotalHeight: CGFloat { - return statusBarHeight + navBarHeight - } - - static var tabBarFrame: CGRect { - if let vc = key?.rootViewController as? UITabBarController { - if #available(iOS 11.0, *) { - return vc.tabBar.frame - } else { - return .zero - } - } else { - return .zero - } - } - - static var tabBarHeight: CGFloat { - if #available(iOS 11.0, *) { - return tabBarTotalHeight - (key?.safeAreaInsets.bottom ?? 0) - } else { - return tabBarTotalHeight - } - } - - static var tabBarTotalHeight: CGFloat { - return tabBarFrame.height - } - - class func getTopViewController(base: UIViewController? = UIWindow.getTopDisplayWindow()?.rootViewController) -> UIViewController? { - if let nav = base as? UINavigationController { - return getTopViewController(base: nav.viewControllers.last) - - } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController { - return getTopViewController(base: selected) - - } else if let presented = base?.presentedViewController { - return getTopViewController(base: presented) - } - return base - } - - class func getTopNavigationController() -> CLNavigationController?{ - return getTopViewController()?.navigationController as? CLNavigationController - } - - class func getTopDisplayWindow() -> UIWindow? { - var theWindow = applicationKey - let windows = UIApplication.shared.windows - -// windows.forEach { w in -// dlog("获取的windows getTopDisplayWindow = \(w.self), isKey = \(w.isKeyWindow)") -// } - - for window in windows.reversed() { - // 需要过滤掉的类型 - // ⚠️ Swift创建的Window为{ProjectName}.NoticeWindow,带命名空间,注意判断逻辑的写法:NSClassFromString("LegendTeam.NoticeWindow")or window.isKind(of: NoticeWindow.self) -// if window.isKind(of: NoticeWindow.self) { -// continue -// } - -// UIRemoteKeyboardWindow -// UITextEffectsWindow - if let class1 = NSClassFromString("UIRemoteKeyboardWindow") { - if window.isKind(of: class1) { - //dlog("过滤掉的window UIRemoteKeyboardWindow") - continue - } - } - if let class2 = NSClassFromString("UITextEffectsWindow") { - if window.isKind(of: class2) { - //dlog("过滤掉的window UITextEffectsWindow") - continue - } - } - if let class3 = NSClassFromString("EGChatRoomFloatWindow") { - if window.isKind(of: class3) { - //dlog("过滤掉的window EGChatRoomFloatWindow") - continue - } - } - - if let class4 = NSClassFromString("PhoneFloatWindow") { - if window.isKind(of: class4) { - //dlog("过滤掉的window PhoneFloatWindow") - continue - } - } - - if let class5 = NSClassFromString("PhoneWindow") { - if window.isKind(of: class5) { - //dlog("过滤掉的window PhoneWindow") - continue - } - } - -// if ChatRoomRoute.isMiniMize() { -// if let class4 = NSClassFromString("ChatRoomWindow") { -// if window.isKind(of: class4) { -// dlog("过滤掉的window ChatRoomWindow, 聊天室缩小状态") -// continue -// } -// } -// } - - if !window.isHidden, window.alpha == 1 { - theWindow = window - // dlog("获取的window2 = \(window), type = \(window.self)") - break - } - } - return theWindow - } -} - -extension UIScreen { - static var width: CGFloat { - return UIScreen.main.bounds.width - } - - static var height: CGFloat { - return UIScreen.main.bounds.height - } - - /// 屏幕尺寸(CGSize) - static var size: CGSize { - return UIScreen.main.bounds.size - } - - /// 屏幕比例(宽 / 高) - static var aspectRatio: CGFloat { - return width / height - } - - /// 是否是窄屏(如 iPhone SE) - static var isNarrow: Bool { - return width <= 320 - } -} - -extension UIDevice { - static var isIphoneX: Bool { - if UIDevice.current.userInterfaceIdiom != .phone { - return true - } - if #available(iOS 11.0, *) { - let bottom = UIWindow.safeAreaInsets.bottom - if bottom > 0.0 { - return true - } - } - return false - } - - /// 获取屏幕系数缩放后的值 - static func DP(x: CGFloat) -> CGFloat { - let standardWidth: CGFloat = 375.0 - let scale = UIScreen.width / standardWidth - return x * scale - } - - /// 获取屏幕系数缩放后的值,最大不超过原值 - static func DP(max: CGFloat) -> CGFloat { - let x = DP(x: max) - if x > max { - return max - } else { - return x - } - } -} diff --git a/crush/Crush/Src/Utils/IAPCore/CLPurchase.swift b/crush/Crush/Src/Utils/IAPCore/CLPurchase.swift deleted file mode 100644 index 44133c8..0000000 --- a/crush/Crush/Src/Utils/IAPCore/CLPurchase.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// VIPViewModel.swift -// Crush -// -// Created by Leon on 2025/9/17. -// - -class CLPurchase{ - static let shared = CLPurchase() - - var vipSubscribeProducts = [VIPSubcribeProduct]() - var memberDetailOut : MemberDetailOutput? - // VIP特权列表 - var privileges = [MemberPrivDict]() - - func loadVIPTiersProducts(completion: @escaping ([VIPSubcribeProduct]?) -> Void){ - if vipSubscribeProducts.count > 0 { - completion(vipSubscribeProducts) - return - } - - var params = [String: Any]() - params.updateValue("iOS", forKey: "platform") - params.updateValue("1", forKey: "version") - - WalletProvider.request(.vipTierProducts(params: params), modelType: [VIPSubcribeProduct].self) { result in - switch result { - case .success(let model): - self.vipSubscribeProducts = model ?? [] - completion(model) - case .failure: - completion(nil) - } - } - - } - - func loadCoinTiersProducts(completion: @escaping (IAPTiersResponse?) -> Void){ - var params = [String: Any]() - params.updateValue("iOS", forKey: "platform") - params.updateValue("1", forKey: "version") - - WalletProvider.request(.coinTierProducts(params: params), modelType: IAPTiersResponse.self) { result in - switch result { - case .success(let model): - completion(model) - case .failure: - completion(nil) - } - } - } - - func loadCurrentUserVipDetail(completion: @escaping (MemberDetailOutput?) -> Void){ - WalletProvider.request(.vipDetail, modelType: MemberDetailOutput.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success(let model): - self?.memberDetailOut = model - self?.privileges = model?.memberPrivList ?? [] - completion(model) - case .failure: - completion(nil) - } - } - } - - // MARK: - Funtions - - func showIAPBuyCoinSheet(){ - WalletCore.shared.refreshWallet() // 刷新下余额(注意调整位置) - - let buyCoinSheet = CoinsRechargeSheet() - buyCoinSheet.show() - } - - /// 余额不足,弹出余额弹窗 + chatModel提醒 - func showChatModelsAndIAPEntrySheet(){ - WalletCore.shared.refreshWallet() // 刷新下余额(注意调整位置) - - let buyCoinSheet = ChatModePickInsufficientCoinSheet() - buyCoinSheet.show() - } - - func showVIPSubscribeSheet(){ - let sheet = VIPSubscribeSheet() - sheet.show() - } - - func showBuyPhotoGeneratedCountSheet(){ - let sheet = BuyCreditsSheetView() - sheet.show() - } - - -} diff --git a/crush/Crush/Src/Utils/IAPCore/Help/CLProducts.swift b/crush/Crush/Src/Utils/IAPCore/Help/CLProducts.swift deleted file mode 100755 index ce987d7..0000000 --- a/crush/Crush/Src/Utils/IAPCore/Help/CLProducts.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// ltProducts.swift -// LegendTeam -// -// Created by dong on 2021/12/27. -// - -import Foundation - -class IAPTiersResponse : NSObject, Codable{ - var productList:[IAPProducts]? -} - -class IAPProducts: NSObject, Codable { - var chargeAmount: Int? - var payAmount: CGFloat? - var productId: String? - var giftAmount: Int? - var hot: Bool? - - // MARK: Custom properties - - var currencySymbol: String? - - // additional - var tradeId: String? - - override required init() {} -} - -class VIPSubcribeProduct:NSObject, Codable { - override required init() {} - - var period: PriceType? // e.g. "SUB_YEAR" - var productType: String? // e.g. "SUBSCRIPTION" - var freeDays: Int? // null → Int? - var platform: String? // e.g. "ios" - var productId: String? // e.g. "gg.epal.lab.ios.vip12months" - var discount: String? // e.g. "16.6" - var bundleId: String? // e.g. "com.crushlevel" - var memberType: MemberType? // e.g. "VIP" - var chargeAmount: Int? // e.g. 0 - var payAmount: Int? // e.g. 5779 -} - - -class ChargePreOrder: NSObject, Codable { - /** - WAITPAY(1, "待付款"), - - PAID(2, "已付款"), - - PROCESSING(3, "处理中"), - - FINISHED(4, "交易成功"), - - CLOSED(5, "交易关闭"), - - REFUNDING(6, "退款中"), - - REFUNDED(7, "已退款"), - */ - var tradeStatus: String = "" - var tradeNo: String = "" - - required override init() {} -} diff --git a/crush/Crush/Src/Utils/IAPCore/Help/EGIAPKeyChainStore.h b/crush/Crush/Src/Utils/IAPCore/Help/EGIAPKeyChainStore.h deleted file mode 100755 index e0f6231..0000000 --- a/crush/Crush/Src/Utils/IAPCore/Help/EGIAPKeyChainStore.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// EGIAPKeyChainStore.h -// EGirl -// -// Created by donglyu on 2020/6/24. -// Copyright © 2020 EGirl. All rights reserved. -// - -#import -//#import -#import "UICKeyChainStore.h" -#import "IapModels.h" - - -NS_ASSUME_NONNULL_BEGIN -static NSString *const KeychainServiceKey = @"gg.epal.lab.keychain"; -static NSString *const KeychainServicePreStoreKey = @"gg.epal.lab.keychian.prestore"; - -@interface EGIAPKeyChainStore : UICKeyChainStore - - -- (instancetype)initWithService:(NSString *)service Group:(nullable NSString *)group; - - -/// 持久化新的模型 -- (void)SavePaymentTransactionModel:( NSArray *)models forUserId:(NSInteger)userId; - -/// 移除某个订单的持久化 -- (void)DeletePaymentTransactionModelWithTransactionId:(NSString *)transactionId forUserId:(NSInteger)userId; - -/// 票据验证成功,移除持久化 -//- (void)DeletePaymentTransactionModelWithTransactionId:(NSString *)transactionId TradeNo:(NSString *)tradeNo ForUserId:(NSInteger)userId; - -/// 清理所有空transactionId的数据 -- (void)ClearAllEmptyTransactionIdData:(NSInteger)userId; - -/// 根据 transactionId 找到之前持久化的其他信息 -- (PaymentTransactionModel *)FindPaymentTransactionModelWithTransactionId:(NSString *)transactionId forUserId:(NSInteger)userId; - -/// 某个userId下的所有持久化数据 -- (NSMutableArray *)FetchAllTransactionModelsForUser:(NSInteger)userId error:(NSError *__autoreleasing _Nullable *)error; - - -#pragma mark - pre store part. 兜底方案. 持续缓存product和tradeId的对应关系 - -- (void)StorePreProduct:(NSString *)productId TradeId:(NSString *)tradeId; - -- (BOOL)FindAndMatchTradeToStartVerifyByProudctId:(NSString *)productId BindTransactionId:(NSString *)transactionId ForUserId:(NSInteger)userId; - -@end - -NS_ASSUME_NONNULL_END diff --git a/crush/Crush/Src/Utils/IAPCore/Help/EGIAPKeyChainStore.m b/crush/Crush/Src/Utils/IAPCore/Help/EGIAPKeyChainStore.m deleted file mode 100755 index 0f7186e..0000000 --- a/crush/Crush/Src/Utils/IAPCore/Help/EGIAPKeyChainStore.m +++ /dev/null @@ -1,323 +0,0 @@ -// -// EGIAPKeyChainStore.m -// EGirl -// -// Created by donglyu on 2020/6/24. -// Copyright © 2020 EGirl. All rights reserved. -// - -#import "EGIAPKeyChainStore.h" -#import -#import "MJExtension.h" -@interface EGIAPKeyChainStore () - -@property (nonatomic) pthread_mutex_t lock; - -@end - -@implementation EGIAPKeyChainStore - -- (instancetype)initWithService:(NSString *)service Group:(NSString *)group { - EGIAPKeyChainStore *store = [super initWithService:service accessGroup:group]; - pthread_mutex_init(&(_lock), NULL); - // ⚠️移除所有持久化数据 for test -// [self removeItemForKey:KeychainServiceKey]; - return store; -} - -- (void)dealloc { - pthread_mutex_destroy(&_lock); -} - -#pragma mark - public - -- (void)SavePaymentTransactionModel:(NSArray *)models forUserId:(NSInteger)userId { - if (models.count == 0) { - return; - } - - // 将 models 归档. - NSMutableSet *modelsDataSetM = [NSMutableSet setWithArray:[self internalEncodeModels:models]]; - - // 与已有的数据组合存储. - NSMutableArray *modelsExisted = [self FetchAllTransactionModelsForUser:userId error:nil]; - - pthread_mutex_lock(&_lock); - if (modelsExisted.count) { // 本地已持久化的一些 - // 检查一下 keychain 中是否已经存在当前 model. - NSMutableArray *modelsNeedToRemoveExisted = [@[] mutableCopy]; - for (PaymentTransactionModel *modelExisted in modelsExisted) { - for (PaymentTransactionModel *model in models) { - if ([modelExisted.tradeNo isEqualToString:model.tradeNo]) { - [modelsNeedToRemoveExisted addObject:modelExisted]; - NSLog(@"keychain 中已经有: %@, 不用再存一遍.", model); - } - } - } - if (modelsNeedToRemoveExisted.count) { - [modelsExisted removeObjectsInArray:modelsNeedToRemoveExisted]; - } - if (modelsExisted.count) { - NSMutableArray *modelsExistedDataM = [self internalEncodeModels:modelsExisted]; - [modelsDataSetM addObjectsFromArray:modelsExistedDataM]; - } - } - - // 存入 keychain. - [self internalSaveModelsData:modelsDataSetM.copy forUser:userId]; - pthread_mutex_unlock(&_lock); -} - -- (NSMutableArray *)FetchAllTransactionModelsForUser:(NSInteger)userId error:(NSError *__autoreleasing _Nullable *)error { - pthread_mutex_lock(&_lock); - NSError *fetchError; - NSData *data = [self dataForKey:KeychainServiceKey error:&fetchError]; - if (fetchError) { - NSLog(@"⚠️get keychain data error: %@", fetchError); - } - pthread_mutex_unlock(&_lock); - - if (data == nil) { - //DLog(@"iap keychain data is nil"); - return nil; - } - - pthread_mutex_lock(&_lock); - NSDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:data]; - pthread_mutex_unlock(&_lock); - - NSMutableArray *modelsData = nil; - if (![dict.allKeys containsObject:[NSString stringWithFormat:@"%ld", (long)userId]]) { - NSLog(@"iap keychain user's data is nil"); - return nil; - } - - pthread_mutex_lock(&_lock); - NSString *userIdStr = [NSString stringWithFormat:@"%ld", (long)userId]; - modelsData = [NSKeyedUnarchiver unarchiveObjectWithData:[dict valueForKey:userIdStr]]; - pthread_mutex_unlock(&_lock); - - pthread_mutex_lock(&_lock); - NSMutableArray *arrM = [NSMutableArray arrayWithCapacity:modelsData.count]; - for (NSData *data in modelsData) { - NSParameterAssert([data isKindOfClass:[NSData class]]); - PaymentTransactionModel *model = [NSKeyedUnarchiver unarchiveObjectWithData:data]; - if (model) { - [arrM addObject:model]; - } - } - pthread_mutex_unlock(&_lock); - - NSLog(@"💰💰💰 已持久化的数据数: %ld个 分别是:", (long)arrM.count); - -#ifdef DEBUG - for (PaymentTransactionModel *m in arrM) { - NSLog(@"🛒tradeId: %@ 🧰Id: %@ time:%@", m.tradeNo, m.transactionId, @(m.tradeTime)); - } - -#endif - return [arrM mutableCopy]; -} - -- (void)DeletePaymentTransactionModelWithTransactionId:(NSString *)transactionId forUserId:(NSInteger)userId { - if (transactionId.length == 0) { - NSAssert(false, @"transactionId is nil"); - return; - } - - NSError *error; - NSMutableArray *modelsM = [self FetchAllTransactionModelsForUser:userId error:&error].mutableCopy; - NSLog(@"✅✅✅✅✅ 票据验证 • 大成功 ✅✅✅✅✅"); - [modelsM.reverseObjectEnumerator.allObjects enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - PaymentTransactionModel *per = obj; - if ([per.transactionId isEqualToString:transactionId]) { - NSLog(@"移除持久化 transactionId:%@ tradeNo:%@", per.transactionId, per.tradeNo); - [modelsM removeObject:per]; - } - }]; - - NSMutableArray *modelsExistedDataM = [self internalEncodeModels:modelsM]; - - [self internalSaveModelsData:modelsExistedDataM forUser:userId]; -} - -- (PaymentTransactionModel *)FindPaymentTransactionModelWithTransactionId:(NSString *)transactionId forUserId:(NSInteger)userId { - if (transactionId.length == 0) { - NSAssert(false, @"transactionId is nil"); - return nil; - } - - NSError *error; - NSMutableArray *modelsM = [self FetchAllTransactionModelsForUser:userId error:&error].mutableCopy; - - for (PaymentTransactionModel *per in modelsM) { - if ([per.transactionId isEqualToString:transactionId]) { - return per; - break; - } - } - - return [PaymentTransactionModel new]; -} - -#pragma mark 预缓存product和trasaction part - -- (void)StorePreProduct:(NSString *)productId TradeId:(NSString *)tradeId { - if (productId.length == 0 && tradeId.length == 0) { - NSLog(@"❌ prestore product:%@ tradeId: %@", productId, tradeId); - return; - } - - pthread_mutex_lock(&_lock); - NSData *data = [self dataForKey:KeychainServicePreStoreKey]; - pthread_mutex_unlock(&_lock); - - NSData *finalData = nil; - - if (data == nil) { - NSDictionary *dict = @{ productId: tradeId }; - finalData = [dict mj_JSONData]; - DLog(@"pre store data:%@", finalData); - } else { - NSMutableDictionary *dict = [[data mj_JSONObject] mutableCopy]; - DLog(@"💰keychain中持久的product和tradeid dict: %@", dict); - [dict setObject:tradeId forKey:productId]; - finalData = [dict mj_JSONData]; - } - - if (finalData) { - NSError *error; - pthread_mutex_lock(&_lock); - [self setData:finalData forKey:KeychainServicePreStoreKey]; - pthread_mutex_unlock(&_lock); - if (error) { - NSLog(@"pre store error:%@", error.localizedDescription); - } - } else { - NSAssert(false, @""); - } -} - -- (BOOL)FindAndMatchTradeToStartVerifyByProudctId:(NSString *)productId BindTransactionId:(NSString *)transactionId ForUserId:(NSInteger)userId { - NSString *tradeId = [self getTradeIdBy:productId]; - if (tradeId.length == 0) { - return NO; - } - - PaymentTransactionModel *model = [[PaymentTransactionModel alloc] init]; - model.tradeNo = tradeId; - model.transactionId = transactionId; - model.productId = productId; - model.userId = userId; - model.tradeTime = ([[NSDate date] timeIntervalSince1970] * 1000); - - [self SavePaymentTransactionModel:@[model] forUserId:userId]; - - return YES; -} - -#pragma mark - events - -/// 清理所有空transactionId的数据 -- (void)ClearAllEmptyTransactionIdData:(NSInteger)userId { - NSError *error; - NSMutableArray *modelsM = [self FetchAllTransactionModelsForUser:userId error:&error].mutableCopy; - NSMutableArray *circle = modelsM.mutableCopy; - - __block BOOL isOperted = NO; - [circle.reverseObjectEnumerator.allObjects enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - PaymentTransactionModel *per = obj; - if (per.transactionId.length == 0) { - isOperted = YES; - [modelsM removeObject:per]; - } - }]; - - if (isOperted) { - NSMutableArray *modelsExistedDataM = [self internalEncodeModels:modelsM]; - if (modelsM.count) { - NSLog(@"--- 💰💰Clear All Empty transactionId data done!💰💰 ---"); - for (PaymentTransactionModel *m in modelsM) { - DLog(@"🏥tradeId: %@ 🍂transactionId: %@ ", m.tradeNo, m.transactionId); - } - } - - [self internalSaveModelsData:modelsExistedDataM forUser:userId]; - } -} - -#pragma mark pre store - -- (NSString *)getTradeIdBy:(NSString *)productId { - if (productId.length == 0) { - return nil; - } - - pthread_mutex_lock(&_lock); - NSData *data = [self dataForKey:KeychainServicePreStoreKey]; - pthread_mutex_unlock(&_lock); - - NSDictionary *dict = [data mj_JSONObject]; - if (dict && [dict objectForKey:productId]) { - NSString *tradeId = [dict objectForKey:productId]; - return tradeId; - } - - return nil; -} - -#pragma mark - private - -/// 保存某个用户的 交易验证数据 -- (void)internalSaveModelsData:(nullable NSArray *)modelsData forUser:(NSInteger)userId { - NSData *setData = modelsData.count ? [NSKeyedArchiver archivedDataWithRootObject:modelsData] : nil; // mj_jsonData - - // 原本数据 - NSData *dictData = [self dataForKey:KeychainServiceKey]; - - // -> NSDictionary - NSMutableDictionary *dictM; - if (dictData) { - dictM = [NSKeyedUnarchiver unarchiveObjectWithData:dictData]; - } - if (!dictM) { - dictM = [NSMutableDictionary dictionary]; - } - - NSString *userIdStr = [NSString stringWithFormat:@"%ld", (long)userId]; - if (setData) { - [dictM setObject:setData forKey:userIdStr]; - } else { // 清空某个userId的数据 - if ([dictM.allKeys containsObject:userIdStr]) { - [dictM removeObjectForKey:userIdStr]; - } - } - - NSData *data = dictM.count ? [NSKeyedArchiver archivedDataWithRootObject:dictM] : nil; // [NSKeyedArchiver archivedDataWithRootObject:dictM] - - NSLog(@"💰💰💰 当前持久化的票据: %@", dictM); - - // 先删除, 后存储. - [self removeItemForKey:KeychainServiceKey]; - if (data) { - NSError *error; - [self setData:data forKey:KeychainServiceKey error:&error]; - if (error) { - DLog(@"💰❌ save models to keychain error: %@", error); - } - } -} - -- (NSMutableArray *)internalEncodeModels:(NSArray *)models { - NSMutableArray *modelsDataM = [NSMutableArray array]; - for (PaymentTransactionModel *model in models) { - NSData *data = [NSKeyedArchiver archivedDataWithRootObject:model]; - NSParameterAssert(data); - if (data) { - [modelsDataM addObject:data]; - } - } - return modelsDataM; -} - -@end diff --git a/crush/Crush/Src/Utils/IAPCore/Help/IAPProducts.h b/crush/Crush/Src/Utils/IAPCore/Help/IAPProducts.h deleted file mode 100755 index 5362c50..0000000 --- a/crush/Crush/Src/Utils/IAPCore/Help/IAPProducts.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// IAPProducts.h -// EGirl -// -// Created by donglyu on 2020/6/28. -// Copyright © 2020 EGirl. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -/// App l后端配置的内购项目 -//@interface IAPProducts : NSObject -// -//@property (nonatomic, assign) NSInteger payAmount; -///// 只在充值buff时,此值有意义 -//@property (nonatomic, assign) NSInteger chargeAmount; -//@property (nonatomic, assign) NSInteger id; -// -///// gg.epal.buff_0004 等 -//@property (nonatomic, copy) NSString *productId; -// -///// 记录的trade Id(如果有下单) 退出页面后和付款后清空 -//@property (nonatomic, copy) NSString *tradeId; -// -//// --- custom -// -//@property (nonatomic, copy) NSString *currencySymbol; -// -// -//@end - -NS_ASSUME_NONNULL_END diff --git a/crush/Crush/Src/Utils/IAPCore/Help/IAPProducts.m b/crush/Crush/Src/Utils/IAPCore/Help/IAPProducts.m deleted file mode 100755 index a78b2fe..0000000 --- a/crush/Crush/Src/Utils/IAPCore/Help/IAPProducts.m +++ /dev/null @@ -1,13 +0,0 @@ -// -// IAPProducts.m -// EGirl -// -// Created by donglyu on 2020/6/28. -// Copyright © 2020 EGirl. All rights reserved. -// - -#import "IAPProducts.h" - -//@implementation IAPProducts -// -//@end diff --git a/crush/Crush/Src/Utils/IAPCore/Help/IapModels.h b/crush/Crush/Src/Utils/IAPCore/Help/IapModels.h deleted file mode 100755 index 3f8d073..0000000 --- a/crush/Crush/Src/Utils/IAPCore/Help/IapModels.h +++ /dev/null @@ -1,56 +0,0 @@ -// -// IapStoreModel.h -// EGirl -// -// Created by donglyu on 2020/6/24. -// Copyright © 2020 EGirl. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - 验证请求模型 -@class IapStoreModel; -@interface IapVerifyModel : NSObject - -@property (nonatomic, strong) NSArray *transactions; - -@property (nonatomic, copy) NSString *receipt; - -@end - - -/// 请求模型 -@interface IapStoreModel : NSObject - -@property (nonatomic, assign) NSInteger userId; - -/// ⭐required -//@property (nonatomic, copy) NSString *transactionId; -/// ⭐required 交易Id -//@property (nonatomic, copy) NSString *tradeNo; - -@end -#pragma mark - 持久化 - -/// 持久化此对象实例 -@interface PaymentTransactionModel : NSObject - -@property (nonatomic, assign) NSInteger userId; - -@property (nonatomic, copy) NSString *transactionId; -/// 交易Id -@property (nonatomic, copy) NSString *tradeNo; - -/// 商品Id -@property (nonatomic, copy) NSString *productId; - -@property (nonatomic, assign) NSInteger tradeTime; - -/// 主要是transactionId -- (void)UpdateDataForm:(PaymentTransactionModel *)model; - -@end - -NS_ASSUME_NONNULL_END diff --git a/crush/Crush/Src/Utils/IAPCore/Help/IapModels.m b/crush/Crush/Src/Utils/IAPCore/Help/IapModels.m deleted file mode 100755 index c9383e5..0000000 --- a/crush/Crush/Src/Utils/IAPCore/Help/IapModels.m +++ /dev/null @@ -1,64 +0,0 @@ -// -// IapStoreModel.m -// EGirl -// -// Created by donglyu on 2020/6/24. -// Copyright © 2020 EGirl. All rights reserved. -// - -#import "IapModels.h" - -@implementation IapVerifyModel - - - -@end - -@implementation IapStoreModel - -@end - -@implementation PaymentTransactionModel - -- (void)UpdateDataForm:(PaymentTransactionModel *)model{ - self.transactionId = model.transactionId; - -} - -- (instancetype)init -{ - self = [super init]; - if (self) { - _tradeNo = @""; - } - return self; -} - - -- (instancetype)initWithCoder:(NSCoder *)coder -{ - self = [super init]; - if (self) { - _userId = [coder decodeIntegerForKey:@"userId"]; - _transactionId = [coder decodeObjectForKey:@"transactionId"]; - _tradeNo = [coder decodeObjectForKey:@"tradeNo"]; - if (_tradeNo == nil || _tradeNo.length == 0){ - _tradeNo = @""; - } - _productId = [coder decodeObjectForKey:@"productId"]; - if ([coder containsValueForKey:@"tradeTime"]) { - _tradeTime = [coder decodeIntegerForKey:@"tradeTime"]; - } - } - return self; -} - -- (void)encodeWithCoder:(NSCoder *)coder{ - [coder encodeInteger:self.userId forKey:@"userId"]; - [coder encodeObject:self.transactionId forKey:@"transactionId"]; - [coder encodeObject:self.tradeNo forKey:@"tradeNo"]; - [coder encodeObject:self.productId forKey:@"productId"]; - [coder encodeInteger:self.tradeTime forKey:@"tradeTime"]; -} - -@end diff --git a/crush/Crush/Src/Utils/IAPCore/IAPCore.swift b/crush/Crush/Src/Utils/IAPCore/IAPCore.swift deleted file mode 100755 index f19f534..0000000 --- a/crush/Crush/Src/Utils/IAPCore/IAPCore.swift +++ /dev/null @@ -1,432 +0,0 @@ -// -// IAPCore.swift -// LegendTeam -// -// Created by dong on 2021/12/22. -// - -import Foundation -import StoreKit - -typealias IdsCallbackBlcok = ((_ iapIds: [String]) -> Void) -class IAPCore: NSObject { - public static let shared = IAPCore() - - var iapProducts: [SKProduct] = [SKProduct]() - - var appProducts: [IAPProducts] = [IAPProducts]() - - var buyingTransModel: PaymentTransactionModel? - var buyingSKProduct: SKProduct? // 临时记录要买的商品(苹果实例) - - var isContinueBuyProductId: String? - var isContinueBuyTradeId: String? - - var refreshedProductsFromIapBlock: IdsCallbackBlcok? - - /// 未登录下,持久化票据,等待用户登录后去校验 - var toBeCheckTransaction: SKPaymentTransaction? - - // keychain - var keychainStore: EGIAPKeyChainStore = EGIAPKeyChainStore(service: KeychainServiceKey, group: nil) - - override init() { - super.init() - NotificationCenter.default.addObserver(self, selector: #selector(notiLoginSuccess), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: - public - // 🔥🔥需在AppDeletgate中调用此方法 - func check() { - SKPaymentQueue.default().add(self) - } - - public func requestProducts(_ products: [IAPProducts], resultblock block: IdsCallbackBlcok?) { - appProducts = products - - if products.count == 0 { - block?([]) - return - } - - // AppStore.canMakePayments: iOS 15+ - if SKPaymentQueue.canMakePayments() { - refreshedProductsFromIapBlock = block - var requestIds = [String]() - for per in products { - if let productId = per.productId{ - requestIds.append(productId) - } - } - - let skRequest = SKProductsRequest(productIdentifiers: Set(requestIds)) - skRequest.delegate = self - skRequest.start() - - } else { - // 用户禁止应用内付费购买。 toast。。。 - dlog("💰❌用户禁止应用内付费购买") - } - } - - public func addPayProductId(productId: String, tradeId: String) { - let iaps = iapProducts - guard iaps.count > 0 else { - isContinueBuyProductId = productId - isContinueBuyTradeId = tradeId - return - } - - var tryBuySKProduct: SKProduct? - for per in iaps { - if per.productIdentifier == productId { - tryBuySKProduct = per - buyingSKProduct = per - } - } - - if let buySK = tryBuySKProduct { - Hud.showIndicator() - addPayment(buySk: buySK, tradeId: tradeId) - } else { - dlog("商品不存在") - buyingSKProduct = nil - } - } - - // MARK: helper - - func addPayment(buySk: SKProduct, tradeId: String) { - if !UserCore.shared.checkUserLoginIfNotPushUserToLogin() { - return - } - - let payment: SKPayment = SKPayment(product: buySk) - SKPaymentQueue.default().add(payment) - - // 持久化购买 - - let transModel = PaymentTransactionModel() - transModel.tradeNo = tradeId - transModel.productId = buySk.productIdentifier - transModel.tradeTime = Date().timeStamp - buyingTransModel = transModel - if let userId = UserCore.shared.user?.userId { - transModel.userId = userId - } - - keychainStore.storePreProduct(transModel.productId, tradeId: transModel.tradeNo) - } - - private func finishATransation(transaction: SKPaymentTransaction?) { - guard let trans = transaction else { - return - } - - if trans.transactionState == .purchasing { - return - } - - SKPaymentQueue.default().finishTransaction(trans) - } - - private func restoreTransaction(transaction: SKPaymentTransaction?) { - // nothing - } - - private func transactionPurchased(transaction: SKPaymentTransaction?) { - guard let trans = transaction, let transId = trans.transactionIdentifier else { - return - } - - var model: PaymentTransactionModel? - let userId = UserCore.shared.user?.userId ?? 0 - if let buyingTrans = buyingTransModel { // 正在操作的购买 - model = buyingTrans - buyingTrans.transactionId = transId - - keychainStore.savePaymentTransactionModel([buyingTrans], forUserId: userId) - - } else { // 一般 是监听到的购买信息(重启后).需要重新去验证 - model = keychainStore.findPaymentTransactionModel(withTransactionId: transId, forUserId: userId) - if model == nil { - let result = keychainStore.findAndMatchTradeToStartVerify(byProudctId: trans.payment.productIdentifier, bindTransactionId: transId, forUserId: userId) - - if result { - model = keychainStore.findPaymentTransactionModel(withTransactionId: transId, forUserId: userId) - } - } - } - - guard let productId = model?.productId else{ - assert(false) - return - } - - if productId.contains("coin_ios") { - verifyPurchased(purchased: model, transtion: trans) - }else if productId.contains("vip"){ - verifyAutoRenewPurchased(purchased: model, transtion: trans) - } - else{ - dlog("❌❌❌不支持的transaction:\(String(describing: transaction))"); - // 【测试用】关掉交易,一般是注释掉的 - finishATransation(transaction: trans) - } - } - - /// 向后端服务器验证购买buff商品 - private func verifyPurchased(purchased: PaymentTransactionModel?, transtion: SKPaymentTransaction?) { - guard let model = purchased, model.tradeNo.count > 0, let transId = transtion?.transactionIdentifier, transId.count > 0 else { - return - } - - if UserCore.shared.isLogin() == false { - toBeCheckTransaction = transtion - return - } - - guard let receiptURL = Bundle.main.appStoreReceiptURL else { - return - } - // let isSandbox = receiptURL.absoluteString.hasSuffix("sandboxReceipt") - let receiptData = try! Data(contentsOf: receiptURL) - let recieptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) - - var params = [String:Any]() - - let transactions = [transId:model.tradeNo] - params.updateValue(recieptString, forKey: "receipt") - params.updateValue(transactions, forKey: "transactions") - Hud.showIndicator() - // 上报票据 - WalletProvider.request(.iapUploadCoinReceipt(params: params), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.doWorkAfterVerify(transaction: transtion, tradeNoLT: model.tradeNo, isVIPAutoRenew: false) - case .failure: - self?.doAlertUser() - break - } - } - - /* - PayProvider.request(.verifyiapBuy(receipt: recieptString, signature: "", transaction: transId, tradeNo: model.tradeNo), modelType: String.self) { [weak self] result in - Hud.hideInidcator() - switch result { - case .success: - self?.doWorkAfterVerify(transaction: transtion, tradeNoLT: model.tradeNo) - break - case .failure: - self?.doAlertUser() - break - } - } - */ - } - - private func verifyAutoRenewPurchased(purchased: PaymentTransactionModel?, transtion: SKPaymentTransaction?) { - guard let model = purchased, model.tradeNo.count > 0, let transId = transtion?.transactionIdentifier, transId.count > 0 else { - return - } - - if UserCore.shared.isLogin() == false { - toBeCheckTransaction = transtion - return - } - - guard let receiptURL = Bundle.main.appStoreReceiptURL else { - return - } - - let receiptData = try! Data(contentsOf: receiptURL) - let recieptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) - - var params = [String:Any]() - - params.updateValue(recieptString, forKey: "receipt") - params.updateValue(model.productId, forKey: "productId") - - Hud.showIndicator() - // 上报票据 - WalletProvider.request(.iapUploadSubscribeAppleReceipt(params: params), modelType: EmptyModel.self) {[weak self] result in - Hud.hideIndicator() - switch result { - case .success: - self?.doWorkAfterVerify(transaction: transtion, tradeNoLT: model.tradeNo, isVIPAutoRenew: true) - case .failure: - self?.doAlertUser() - break - } - } - - } - - // MARK: noti - - @objc func notiLoginSuccess() { - transactionPurchased(transaction: toBeCheckTransaction) - } - - // MARK: - helper - - private func doWorkAfterVerify(transaction: SKPaymentTransaction?, tradeNoLT: String?, isVIPAutoRenew: Bool = false) { - guard let userId = UserCore.shared.user?.userId, let trans = transaction, let transId = trans.transactionIdentifier, let tradeNo = tradeNoLT else { - return - } - keychainStore.deletePaymentTransactionModel(withTransactionId: transId, forUserId: userId) - finishATransation(transaction: trans) - - - WalletCore.shared.refreshWallet(block: nil) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if isVIPAutoRenew { - UserCore.shared.refreshUserInfo{ result in - NotificationCenter.post(name: .vipStateChange, object: tradeNo, userInfo: nil) - } - } else { - NotificationCenter.post(name: .chargeDonePushTradeId, object: tradeNo, userInfo: nil) - } - - - } - - // Adjust -// if buyingSKProduct != nil { -// if let currencyCode = buyingSKProduct?.priceLocale.currencyCode, let priceDouble = buyingSKProduct?.price.doubleValue { -// #warning("comfirm") -// //AdjustEvent.eventPayment(price: priceDouble, currencyCode: currencyCode, transactionId: transId) -// } -// } - - buyingTransModel = nil - } - - private func doAlertUser() { -// let alert = Alert(title: R.string.localizable.notice.localized(), text: R.string.localizable.charge_not_verify_correctly_desc.localized()) -// let gotAction = AlertAction(title: R.string.localizable.gotit.localized(), actionStyle: .confirm, block: nil) -// alert.addAction(gotAction) -// alert.show() - - let alert = Alert(title: "Tips", text: "After the Coins is charged, it may take some time to arrive, please pay attention to the wallet details.") - let action1 = AlertAction(title: "Got it", actionStyle: .confirm) { - - } - alert.addAction(action1) - alert.show() - } -} - -extension IAPCore: SKProductsRequestDelegate { - func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { - dlog("--------------收到产品反馈消息---------------------") - DispatchQueue.main.async { - // Hud hideall hud - Hud.hideIndicator() - } - - let iapPs = response.products - iapProducts = iapPs - - if iapPs.count == 0 { - dlog("--------------没有商品------------------") - DispatchQueue.main.async { [weak self] in - // hud show Recharge is not available, please try again later - Hud.toast(str: "Recharge is not available, please try again later") - self?.refreshedProductsFromIapBlock?([]) - } - - return - } - - var iapProductIdsArrayM = [String]() - for appPer in appProducts { - for skPer in iapProducts { - if skPer.productIdentifier == appPer.productId { - let formatter = NumberFormatter() - formatter.numberStyle = .currency // .CurrencyStyle - formatter.locale = skPer.priceLocale - let currencyString = formatter.currencySymbol//internationalCurrencySymbol - appPer.currencySymbol = currencyString ?? "" - appPer.payAmount = CGFloat(skPer.price.floatValue) - iapProductIdsArrayM.append(skPer.productIdentifier) - continue - } - } - } - - DispatchQueue.main.async {[weak self] in - self?.refreshedProductsFromIapBlock?(iapProductIdsArrayM) - } - - // 继续购买 - if let tradeId = isContinueBuyTradeId, tradeId.count > 0, let productId = isContinueBuyProductId, productId.count > 0 { - addPayProductId(productId: productId, tradeId: tradeId) - isContinueBuyTradeId = nil - isContinueBuyProductId = nil - } - } - - func request(_ request: SKRequest, didFailWithError error: Error) { - dlog("------------------错误-----------------:\(error)") - DispatchQueue.main.async { - Hud.hideIndicator() - Hud.toast(str: "\(error.localizedDescription)") - } - } - - func requestDidFinish(_ request: SKRequest) { - dlog("------------反馈信息结束-----------------") - } -} - -// MARK: - 💰💰💰 SKPaymentTransactionObserver 💰💰💰 - -extension IAPCore: SKPaymentTransactionObserver { - func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - let userId = UserCore.shared.user?.userId ?? 0 - for trans in queue.transactions { - switch trans.transactionState { - case .purchasing: -// #warning("hud show in mainqueue") - DispatchQueue.main.async { - Hud.showIndicator() - } - dlog("->💰🚀purchasing....") - break - case .purchased: - transactionPurchased(transaction: trans) - break - case .failed: - DispatchQueue.main.async { - Hud.hideIndicator() - } - dlog("->💰⚠️Purchase failed....\n⚠️error:\(trans.error?.localizedDescription ?? "") \n⚠️transactionId: \(trans.transactionIdentifier ?? "")") - - if trans.error != nil { - guard let transId = trans.transactionIdentifier else { - dlog("💰⚠️⚠️⚠️ Finish掉正确取消或失败的订单 ⚠️⚠️⚠️") - finishATransation(transaction: trans) - return - } - - if transId.count == 0 || keychainStore.findPaymentTransactionModel(withTransactionId: trans.transactionIdentifier!, forUserId: userId).tradeNo.count == 0 { - dlog("💰⚠️⚠️⚠️ Finish掉正确取消或失败的订单 ⚠️⚠️⚠️") - finishATransation(transaction: trans) - } - } - break - case .restored: - restoreTransaction(transaction: trans) - break - default: - break - } - } - } -} diff --git a/crush/Crush/Src/Utils/LanguagesUtils.swift b/crush/Crush/Src/Utils/LanguagesUtils.swift deleted file mode 100755 index 9e11246..0000000 --- a/crush/Crush/Src/Utils/LanguagesUtils.swift +++ /dev/null @@ -1,254 +0,0 @@ -// -// LanguagesUtils.swift -// LegendTeam -// -// Created by dong on 2022/5/18. -// - -import Foundation -//import Lottie -import UIKit -import RswiftResources - -enum Languages: String, Codable { - case id - case en - case vi - case fil - case ms - case th -} - -/// api manager.. etc. -class LanguagesUtils { - /// default languages order - // static var supportLanguages:[Languages] = [.en, .id, .ms, .vi, .fil, .th] - - static func initailLanguageSetting() { -// #warning("test to do") -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { -// Languages.preferedLans = LanguagesUtils.getSupportSortedLanguages() -// LanguagesUtils.showAppLanugagesPickVc() -// } -// return - - if let lan = AppCache.fetchCache(key: CacheKey.userAppLanguage.rawValue, type: Languages.self) { - Languages.preferedLans = [lan] - } else { - if UserCore.shared.isLogin() { // 针对老用户(版本更新前已经注册的老用户) - AppCache.cache(key: CacheKey.userAppLanguage.rawValue, value: Languages.id) - Languages.preferedLans = [Languages.id] - } else { - // 先按系统语言显示 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { - Languages.preferedLans = LanguagesUtils.getSupportSortedLanguages() - LanguagesUtils.showAppLanugagesPickVc() - } - } - } - } - - static func isInitialLanuageAlreadySetting()-> Bool{ - if (AppCache.fetchCache(key: CacheKey.userAppLanguage.rawValue, type: Languages.self) != nil || UserCore.shared.isLogin()) { - return true - }else{ - return false - } - } - - static func showAppLanugagesPickVc() { -// let vc = LanguagesPrimarySetupController() -// guard let rootVc = UIWindow.applicationKey?.rootViewController else { -// assert(false) -// return -// } -// -// rootVc.addChild(vc) -// rootVc.view.addSubview(vc.view) -// -// vc.view.snp.makeConstraints { make in -// make.edges.equalToSuperview() -// } - } - - /// 语言选择sheep vc, 语言顺序按固定默认顺序了 -// @discardableResult -// static func showAppLanugagesPicker() -> SheetPopListSelectView { -// -// } - - // MARK: - helper - - /// 系统的语言顺序 - static func getSystemLanguagesList() -> [String] { - let lanugages = Locale.preferredLanguages.compactMap { $0.components(separatedBy: "-").first } - return lanugages - } - - /// 默认顺序, 用在系统设置中,去设置 - static func languagesDict() -> [Languages] { - return [.en, .id, .ms, .vi, .fil, .th] - } - - /// 排序过后的(系统语言中有的,放到前面去),用在用户首次设置 - static func getSupportSortedLanguages() -> [Languages] { - // default顺序 - let defaultOrderLanguages: [Languages] = LanguagesUtils.languagesDict() - let systemLanguages = LanguagesUtils.getSystemLanguagesList() // ["en", "zh", "id"] - var matchedArray: [Languages] = [] - let matched = systemLanguages.filter { // "en", "id" - for per in defaultOrderLanguages { - if $0 == per.rawValue { - matchedArray.append(per) - return true - } - } - return false - } - - let otherLanguages = defaultOrderLanguages.filter { - !matched.contains($0.rawValue) - } - - return matchedArray + otherLanguages - } - - static func deviceSavePreferLanuage(lan: Languages) { - Languages.preferedLans = [lan] - AppCache.cache(key: CacheKey.userAppLanguage.rawValue, value: lan) - // 加载App其他相关信息 - // ... - - } - - static func restartWindow() { - if let appdelegate = UIApplication.shared.delegate as? AppDelegate { - //appdelegate.setupWindowRootController() - } else { - assert(false) - } - NotificationCenter.post(name: .appLanugageChanged) - } -} - -extension Languages { - /// 初始时尝试从配置中拉取 - public static var preferedLans: [Languages] = [Languages.en] - - // 🔥 RSwift Library会调用 - public static func preferLanguages() -> [String] { - let preferLansCode = preferedLans.map { $0.rawValue } - return preferLansCode + [Languages.en.rawValue] // - } - - public static func localizedAppLanguage() -> String { - let lan = preferedLans.first ?? .en - - return Languages.localizedLang(lan: lan) - } - - // MARK: - helper - - public static func localizedLang(lan: Languages) -> String { - return "en" - } - - public static func selfLanDesc(lan: Languages) -> String { - return "English" - } - - static func regionCode(lan: Languages) -> String { - return "en" - } - - static func localRegionCode() -> String { - let lan = preferedLans.first ?? .en - return Languages.regionCode(lan: lan) - } -} - -@objcMembers class OCLanuagesUtils: NSObject { - @objc static func localized(key: String, tableName: String, arguments: [String]?) -> String { - guard let (locale, bundle) = StringResource.localeBundle(tableName: tableName, preferredLanguages: Languages.preferLanguages()) else { - return key - } - let format = NSLocalizedString(key, tableName: tableName, bundle: bundle, comment: "") - return String(format: format, locale: locale, arguments: arguments ?? []) - } -} - -extension StringResource { - fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap { Locale(identifier: $0) } ?? Locale.current - fileprivate static let hostingBundle = Bundle.main - - public func localized(_ values: [String] = [""]) -> String { - // 替换规则说明: R.string.localizable.home.localized() -> R.string.localizable.home.localized.localized() - guard let (locale, bundle) = StringResource.localeBundle(tableName: tableName, preferredLanguages: Languages.preferLanguages()) else { - return key.description - } - - let format = NSLocalizedString(key.description, tableName: tableName, bundle: bundle, comment: "") - return String(format: format, locale: locale, arguments: values) // String(format: format, locale: locale, values) - } - - /// Find first language and bundle for which the table exists - fileprivate static func localeBundle(tableName: String, preferredLanguages: [String]) -> (Foundation.Locale, Foundation.Bundle)? { - // Filter preferredLanguages to localizations, use first locale - var languages = preferredLanguages - .map { Locale(identifier: $0) } - .prefix(1) - .flatMap { locale -> [String] in - if hostingBundle.localizations.contains(locale.identifier) { - if let language = locale.languageCode, hostingBundle.localizations.contains(language) { - return [locale.identifier, language] - } else { - return [locale.identifier] - } - } else if let language = locale.languageCode, hostingBundle.localizations.contains(language) { - return [language] - } else { - return [] - } - } - - // If there's no languages, use development language as backstop - if languages.isEmpty { - if let developmentLocalization = hostingBundle.developmentLocalization { - languages = [developmentLocalization] - } - } else { - // Insert Base as second item (between locale identifier and languageCode) - languages.insert("Base", at: 1) - - // Add development language as backstop - if let developmentLocalization = hostingBundle.developmentLocalization { - languages.append(developmentLocalization) - } - } - - // Find first language for which table exists - // Note: key might not exist in chosen language (in that case, key will be shown) - for language in languages { - if let lproj = hostingBundle.url(forResource: language, withExtension: "lproj"), - let lbundle = Bundle(url: lproj) { - let strings = lbundle.url(forResource: tableName, withExtension: "strings") - let stringsdict = lbundle.url(forResource: tableName, withExtension: "stringsdict") - - if strings != nil || stringsdict != nil { - return (Locale(identifier: language), lbundle) - } - } - } - - // If table is available in main bundle, don't look for localized resources - let strings = hostingBundle.url(forResource: tableName, withExtension: "strings", subdirectory: nil, localization: nil) - let stringsdict = hostingBundle.url(forResource: tableName, withExtension: "stringsdict", subdirectory: nil, localization: nil) - - if strings != nil || stringsdict != nil { - return (applicationLocale, hostingBundle) - } - - // If table is not found for requested languages, key will be shown - return nil - } -} diff --git a/crush/Crush/Src/Utils/NaviAlphaHandle.swift b/crush/Crush/Src/Utils/NaviAlphaHandle.swift deleted file mode 100644 index 141483a..0000000 --- a/crush/Crush/Src/Utils/NaviAlphaHandle.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// NaviAlphaHandle.swift -// Crush -// -// Created by Leon on 2025/7/20. -// - -import UIKit - -struct NaviAlphaHandle { - static func changeNaviTitleAlpha(scrollView: UIScrollView, titleLabel: UILabel?) { - guard let label = titleLabel else{ - return - } - let offsetY = scrollView.contentOffset.y - if offsetY >= 0 && offsetY <= 60 { - label.alpha = offsetY / 60.0 - } else if offsetY < 0 { - label.alpha = 0 - } else { - label.alpha = 1 - } - } - - static func changeNaviTitleAlpha(scrollView: UIScrollView, alphaViews: [UIView?]) { - let offsetY = scrollView.contentOffset.y - if offsetY >= 0 && offsetY <= 60 { - for view in alphaViews { - if let v = view{ - v.alpha = offsetY / 60.0 - } - } - } else if offsetY < 0 { - for view in alphaViews { - if let v = view{ - v.alpha = 0 - } - } - } else { - for view in alphaViews { - if let v = view{ - v.alpha = 1 - } - } - } - } - - static func changeNaviViewsAlpha(scrollView: UIScrollView, alphaViews: [UIView?], oppositeViews: [UIView?]) { - let offsetY = scrollView.contentOffset.y - if offsetY >= 0 && offsetY <= 60 { - for view in alphaViews { - if let v = view{ - v.alpha = offsetY / 60.0 - } - } - for view in oppositeViews { - if let v = view{ - v.alpha = 1 - offsetY / 60.0 - } - } - } else if offsetY < 0 { - for view in alphaViews { - if let v = view{ - v.alpha = 0 - } - } - for view in oppositeViews { - if let v = view{ - v.alpha = 1 - } - } - } else { - for view in alphaViews { - if let v = view{ - v.alpha = 1 - } - } - for view in oppositeViews { - if let v = view{ - v.alpha = 0 - } - } - } - } -} diff --git a/crush/Crush/Src/Utils/OCHelper.swift b/crush/Crush/Src/Utils/OCHelper.swift deleted file mode 100644 index b5c2718..0000000 --- a/crush/Crush/Src/Utils/OCHelper.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// OCHelper.swift -// Crush -// -// Created by Leon on 2025/7/17. -// -import UIKit - -@objcMembers class OCHelper: NSObject{ - -} - -@objcMembers class OCColor: NSObject{ - class func csbn() -> UIColor{ - return .c.csbn - } -} - - -@objcMembers class OCFont: NSObject{ - class func tts() -> UIFont{ - return CLSystemToken.font(token: .ttl) - } -} diff --git a/crush/Crush/Src/Utils/PushManager.swift b/crush/Crush/Src/Utils/PushManager.swift deleted file mode 100644 index 9fc77b1..0000000 --- a/crush/Crush/Src/Utils/PushManager.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// PushManager.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import Foundation -import UIKit - -class PushManager: NSObject { - public static let shared = PushManager() - var toSubmitPushToken: String = "" - - override required init() { - super.init() - NotificationCenter.default.addObserver(self, selector: #selector(notiLoginSuccess), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - UNUserNotificationCenter.current().delegate = self - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - static func trySubmmitPushTokenFromNewGet(token: String) { - guard let userId = UserCore.shared.user?.userId else { - PushManager.shared.toSubmitPushToken = token - return - } - - // ... - } - - static func registerNotiPush(application: UIApplication) { - let center = UNUserNotificationCenter.current() - center.delegate = PushManager.shared - center.getNotificationSettings { setting in - if setting.authorizationStatus == .notDetermined { - center.requestAuthorization(options: [.badge, .sound, .alert]) { result, error in - if result { - if !(error != nil) { - // 注册成功 - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } - } - } else { - // 用户不允许推送 - } - } - } else if setting.authorizationStatus == .denied { - // 申请用户权限被拒 - } else if setting.authorizationStatus == .authorized { - // 用户已授权(再次获取dt) - DispatchQueue.main.async { - application.registerForRemoteNotifications() - } - } else { - // 未知错误 - } - } - } - - // MARK: - noti - - @objc private func notiLoginSuccess() { - if toSubmitPushToken.count > 0 { - PushManager.trySubmmitPushTokenFromNewGet(token: toSubmitPushToken) - } - } - - // MARK: - helper - - private func dealPushNotification(userInfo: [AnyHashable: Any]?) { - } - - private func dealLocalNotification(userInfo: [AnyHashable: Any]?) { - // none. - } -} - -extension PushManager: UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - let dict = notification.request.content.userInfo - dlog("☁️前台收到推送:\(dict) ") - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - let dict = response.notification.request.content.userInfo - dlog("☁️后台收到推送:\(dict) ") - - // UserCore.shared.refreshUserInfo(block: nil) - - if let trigger = response.notification.request.trigger { - if trigger is UNPushNotificationTrigger { - dealPushNotification(userInfo: dict) - } else if trigger is UNTimeIntervalNotificationTrigger { - dealLocalNotification(userInfo: dict) - } - } - - completionHandler() - } -} diff --git a/crush/Crush/Src/Utils/Router/AppRouter.swift b/crush/Crush/Src/Utils/Router/AppRouter.swift deleted file mode 100644 index 1798499..0000000 --- a/crush/Crush/Src/Utils/Router/AppRouter.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// AppRouter.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import Foundation - -@objcMembers class AppRouter: NSObject { - static let shared = AppRouter() - var isBlockBackRootVc: Bool = false - - static func goBackRootController(jumpIndex: TabBarItemIndex = .home, block: (() -> Void)? = nil) { - guard let window = UIWindow.applicationKey else { - assert(false) - block?() - return - } - guard let rootVC = window.rootViewController else { - assert(false) - block?() - return - } - - // 取消拦截 -// if AppRouter.shared.isBlockBackRootVc { -// block?() -// return -// } - - AppRouter.shared.isBlockBackRootVc = true - - var presentedController: UIViewController? - if rootVC is UITabBarController { - if let vc = (rootVC as? TabBarController)?.selectedViewController { - presentedController = vc - } - } - var presentedControllers = [UIViewController]() - while presentedController?.presentedViewController != nil { - if let vc = rootVC.presentedViewController { - presentedControllers.append(vc) - presentedController = vc - } - } - if presentedControllers.count > 0 { - dismissControllers(presentedControllers: presentedControllers, index: presentedControllers.count - 1) { - popToRootController(from: rootVC) { - guard let vc = rootVC as? TabBarController else { - AppRouter.shared.isBlockBackRootVc = false - return - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - vc.setSelected(index: jumpIndex) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - block?() - AppRouter.shared.isBlockBackRootVc = false - } - } - } - } - } else { - popToRootController(from: presentedController ?? rootVC, animated: false) { - guard let vc = rootVC as? TabBarController else { - AppRouter.shared.isBlockBackRootVc = false - return - } -// DispatchQueue.main.asyncAfter(deadline: .now() + 0) { // 0.25 - vc.setSelected(index: jumpIndex) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - block?() - AppRouter.shared.isBlockBackRootVc = false - } -// } - } - } - } - - static func dismissControllers(presentedControllers: [UIViewController], index: Int, completion: @escaping (() -> Void)) { - if index < 0 { - completion() - } else { - guard let vc = presentedControllers[safe: index] else { return } - vc.dismiss(animated: false) { - dismissControllers(presentedControllers: presentedControllers, index: index - 1, completion: completion) - } - } - } - - static func popToRootController(from vc: UIViewController, animated: Bool? = true, completion: @escaping (() -> Void)) { - if vc is UINavigationController { - if let navc = vc as? UINavigationController { - navc.popToRootViewController(animated: animated ?? true) - completion() - } - } - if let navc = vc.navigationController { - navc.popToRootViewController(animated: animated ?? true) - completion() - } - } -} - -extension AppRouter{ - static func goCreateEditAIRole(aiId: Int? = nil, aiEditInfo: AIUserModel? = nil) { - if AppDictManager.shared.aiDict == nil{ - Hud.showIndicator() - AppDictManager.shared.loadAIDict { ok in - Hud.hideIndicator() - if ok{ - AppRouter.goCreateEditAIRole(aiId: aiId) - } - } - return - } - - if let id = aiId, id > 0{ // 编辑 - if aiEditInfo == nil{ // 请求编辑用信息 - Hud.showIndicator() - AIRoleProvider.request(.aiInfoMineGet(id: id), modelType: AIUserModel.self) { result in - Hud.hideIndicator() - switch result { - case .success(let success): - if let editInfo = success{ - AppRouter.goCreateEditAIRole(aiId: id, aiEditInfo: editInfo) - } - case .failure: - break - } - } - return - } - } - - - let vc = RoleClassificationSelectController() - vc.viewModel.editAIInfo = aiEditInfo - let navc = CLNavigationController(rootViewController: vc) - UIWindow.getTopViewController()?.present(navc, animated: true) - } - - static func goAIRoleHome(aiId: Int?){ - guard let id = aiId else {return} - let vc = RoleHomePagerController() - vc.aiId = id - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } - - // 首次创建, alert是否创建相册 - static func alertCreateAlbumsToRoleHome(aiId: Int?){ - - let alert = Alert(title: "Create Album", text: "Go to your personal homepage to create albums for your characters, attract interlocutors and increase revenue.") - let action1 = AlertAction(title: "Go", actionStyle: .confirm) { - AppRouter.goAIRoleHome(aiId: aiId) - } - let action2 = AlertAction(title: "Not now", actionStyle: .cancel) - alert.addAction(action1) - alert.addAction(action2) - alert.show() - } - - -} diff --git a/crush/Crush/Src/Utils/Router/AppRouterAIRole.swift b/crush/Crush/Src/Utils/Router/AppRouterAIRole.swift deleted file mode 100644 index e232845..0000000 --- a/crush/Crush/Src/Utils/Router/AppRouterAIRole.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AppRouterAIRole.swift -// Crush -// -// Created by Leon on 2025/9/14. -// - -extension AppRouter{ - static func goAIHeartBeatLevelPage(aiId: Int?){ - guard let id = aiId else{return} - let vc = HeartBeatLevelGridController() - vc.aiId = id - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } -} diff --git a/crush/Crush/Src/Utils/Router/AppRouterChat.swift b/crush/Crush/Src/Utils/Router/AppRouterChat.swift deleted file mode 100644 index 1d27913..0000000 --- a/crush/Crush/Src/Utils/Router/AppRouterChat.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// AppRouterChat.swift -// Crush -// -// Created by Leon on 2025/8/15. -// - -import Foundation - -extension AppRouter{ - static func goNoticeCenter(){ - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} - - let vc = NoticeCenterListController() - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } - - static func goChatVC(accId: String, complete: (() -> Void)? = nil) { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} - - let vc = SessionController(accountID: accId) - let nvc = UIWindow.getTopViewController(base: UIWindow.applicationKey?.rootViewController)?.navigationController - nvc?.pushViewController(vc, animated: true) - complete?() - } - - static func goChatVC(aiId: Int?, complete: (() -> Void)? = nil) { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} - - guard let aiUserId = aiId, let accId = IMUserKit.accountIdWithAIId(aiId: aiUserId) else{return} - - let vc = SessionController(accountID: accId) - let nvc = UIWindow.getTopViewController(base: UIWindow.applicationKey?.rootViewController)?.navigationController - nvc?.pushViewController(vc, animated: true) - complete?() - } - - static func goChatVC(conversationId: String?, complete: (() -> Void)? = nil) { - guard UserCore.shared.checkUserLoginIfNotPushUserToLogin() else{return} - - guard let sessionId = conversationId else{return} - let vc = SessionController(conversationId: sessionId) - let nvc = UIWindow.getTopViewController(base: UIWindow.applicationKey?.rootViewController)?.navigationController - nvc?.pushViewController(vc, animated: true) - complete?() - } -} diff --git a/crush/Crush/Src/Utils/Router/AppRouterCoin.swift b/crush/Crush/Src/Utils/Router/AppRouterCoin.swift deleted file mode 100644 index 8d63788..0000000 --- a/crush/Crush/Src/Utils/Router/AppRouterCoin.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// AppRouterCoin.swift -// Crush -// -// Created by Leon on 2025/8/21. -// - -extension AppRouter{ - static func goWalletCenter(){ - let vc = WalletMainPagerController() - UIWindow.getTopViewController()?.navigationController?.pushViewController(vc, animated: true) - } - - static func goBillList(){ - let vc = BillDetailListController() - UIWindow.getTopViewController()?.navigationController?.pushViewController(vc, animated: true) - } - - static func goVIPCenter(){ - let vc = VIPPrivilegesShowController() - UIWindow.getTopViewController()?.navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/crush/Crush/Src/Utils/Router/AppRouterDiscover.swift b/crush/Crush/Src/Utils/Router/AppRouterDiscover.swift deleted file mode 100644 index 1cfdaa4..0000000 --- a/crush/Crush/Src/Utils/Router/AppRouterDiscover.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// AppRouterDiscover.swift -// Crush -// -// Created by Leon on 2025/9/9. -// - -extension AppRouter{ - static func goLeaderboardPager(){ - let vc = LeaderboardPagerMainController() - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } - -} diff --git a/crush/Crush/Src/Utils/Router/AppRouterOther.swift b/crush/Crush/Src/Utils/Router/AppRouterOther.swift deleted file mode 100644 index 0a863a9..0000000 --- a/crush/Crush/Src/Utils/Router/AppRouterOther.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// AppRouterOther.swift -// Crush -// -// Created by Leon on 2025/9/16. -// - -extension AppRouter { - static func goTermsOfService() { - let urlString = "\(AppConst.h5urlRoot)/policy/tos" - let vc = H5Controller() - guard let url = urlString.urlValue else{return} - vc.loadURL(url: url) - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } - - static func goPrivacyPolicy() { - let urlString = "\(AppConst.h5urlRoot)/policy/privacy" - let vc = H5Controller() - guard let url = urlString.urlValue else{return} - vc.loadURL(url: url) - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } - - static func goRechargeH5(){ - let urlString = "\(AppConst.h5urlRoot)/policy/recharge" - let vc = H5Controller() - guard let url = urlString.urlValue else{return} - vc.loadURL(url: url) - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } - - static func goAboutUs(){ - // https://test.crushlevel.ai/about -// let urlString = "\(AppConst.h5urlRoot)/about" -// let vc = H5Controller() -// guard let url = urlString.urlValue else{return} -// vc.loadURL(url: url) -// UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - - let vc = AboutUsController() - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } - - static func goCreatorIntroduction(){ - let vc = CreatorIntroductionController() - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } -} diff --git a/crush/Crush/Src/Utils/Router/AppRouterUser.swift b/crush/Crush/Src/Utils/Router/AppRouterUser.swift deleted file mode 100644 index b729fee..0000000 --- a/crush/Crush/Src/Utils/Router/AppRouterUser.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// AppRouterUser.swift -// Crush -// -// Created by Leon on 2025/9/4. -// - -extension AppRouter{ - static func goPersonalInformationFill(_ userToken: String) { - let vc = PersonalInformationFillController() - vc.userToken = userToken - UIWindow.getTopNavigationController()?.pushViewController(vc, animated: true) - } -} diff --git a/crush/Crush/Src/Utils/S3/CloudStorage.swift b/crush/Crush/Src/Utils/S3/CloudStorage.swift deleted file mode 100644 index c270e3a..0000000 --- a/crush/Crush/Src/Utils/S3/CloudStorage.swift +++ /dev/null @@ -1,561 +0,0 @@ -// -// CloudStorage.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import AWSS3 -import Foundation -import SwiftDate -// MARK: - CloudStorage - -class CloudStorage { - static let shared = CloudStorage() - - private(set) var uploadAlbumItems: [UploadPhotoM] = [] - private var uploadRoleItems: [UploadPhotoM] = [] - private var uploadHeadImagesItems: [UploadPhotoM] = [] - private var uploadIMImageItems: [UploadPhotoM] = [] - private var uploadAudiosItems: [UploadModel] = [] - - var authAudioData: S3AuthData? - var requestAudioSysTokenData: Date? - var audioTransferUtility: AWSS3TransferUtility? - - private init() { - NotificationCenter.default.addObserver(self, selector: #selector(receiveNetStatusChanged(_:)), name: AppNotificationName.networkChanged.notificationName, object: nil) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - private func needCheckImage(for bucket: BucketS3Enum) -> Bool { - switch bucket { - case .HEAD_IMAGE: // , .ALBUM, .ROLE - return true - default: - return false - } - } - - // MARK: - Public Methods - - func s3BatchAddPhotos(_ photos: [UploadPhotoM], bucket: BucketS3Enum, callback: ((Bool) -> Void)?) { - guard !photos.isEmpty else { - callback?(false) - return - } - - var uploadItems: [Any] = [] - switch bucket { - case .HEAD_IMAGE: - uploadItems = uploadHeadImagesItems - case .ALBUM: - uploadItems = uploadAlbumItems - case .ROLE: - uploadItems = uploadRoleItems - case .IM_IMG: - uploadItems = uploadIMImageItems - case .SOUND, .SOUND_PATH: - uploadItems = uploadAudiosItems - default: - callback?(false) - return - } - - for photo in photos { - photo.isUploading = true - photo.bucketEnum = bucket - if !uploadItems.contains(where: { $0 as? UploadPhotoM === photo }) { - uploadItems.append(photo) - } - } - - doS3UploadPhotos(bucket, photos: photos, block: callback) - } - - func s3AddPhotos(_ photos: [UploadPhotoM], bucket: BucketS3Enum) { - guard !photos.isEmpty else { return } - - var uploadItems: [Any] = [] - switch bucket { - case .HEAD_IMAGE: - uploadItems = uploadHeadImagesItems - case .ALBUM: - uploadItems = uploadAlbumItems - case .ROLE: - uploadItems = uploadRoleItems - case .IM_IMG: - uploadItems = uploadIMImageItems - case .SOUND, .SOUND_PATH: - uploadItems = uploadAudiosItems - default: - return - } - - for photo in photos { - photo.isUploading = true - photo.bucketEnum = bucket - if !uploadItems.contains(where: { $0 as? UploadPhotoM === photo }) { - uploadItems.append(photo) - } - } - - doS3UploadPhotos(bucket, photos: photos, block: nil) - } - - func s3AddUploadAudio(_ model: UploadModel, callback: ((Bool) -> Void)?) { - guard let fileData = model.fileData, fileData.count > 0 else { - assert(false, "fileData should be valid") - callback?(false) - return - } - - model.isUploading = true - if !uploadAudiosItems.contains(where: { $0 === model }) { - uploadAudiosItems.append(model) - } - - if judgeTokenValid(requestAudioSysTokenData){ - guard let authData = authAudioData, let utility = audioTransferUtility else{ - callback?(false) - return - } - dlog("☁️利用之前的token上传Audio: \(model)") - self.s3UploadModel(utility, object: model, auth: authData) { result in - callback?(result) - } - }else{ - requestAuthToken(.SOUND_PATH, suffix: .mp3) { [weak self] result, authData, utility in - guard let self = self, !self.uploadAudiosItems.isEmpty, let s3Utility = utility, let s3AuthData = authData else { - callback?(false) - return - } - - if result { - self.requestAudioSysTokenData = Date() - self.s3UploadModel(s3Utility, object: model, auth: s3AuthData) { result in - callback?(result) - } - } else { - model.isUploading = false - DispatchQueue.main.async { - model.mm_uploadFailed() - } - callback?(false) - } - } - } - } - - func cancelTasks(for bucket: BucketS3Enum) { - switch bucket { - case .ALBUM: - for photo in uploadAlbumItems { - cancelTask(for: photo) - uploadAlbumItems.removeAll { $0 === photo } - } - case .HEAD_IMAGE: - for photo in uploadHeadImagesItems { - cancelTask(for: photo) - uploadHeadImagesItems.removeAll { $0 === photo } - } - case .ROLE: - for model in uploadRoleItems { - cancelTask(for: model) - uploadRoleItems.removeAll { $0 === model } - } - case .IM_IMG: - for model in uploadIMImageItems { - cancelTask(for: model) - uploadIMImageItems.removeAll { $0 === model } - } - case .SOUND, .SOUND_PATH: - for model in uploadAudiosItems{ - cancelTask(for: model) - uploadAudiosItems.removeAll {$0 === model} - } - default: - break - } - } - - // MARK: - Private Methods - - private func doS3UploadPhotos(_ bucket: BucketS3Enum, photos: [UploadPhotoM], block: ((Bool) -> Void)?) { - guard !photos.isEmpty else { - assert(false, "photos count 0") - block?(false) - return - } - - let enableGCDGroup = block != nil - var group: DispatchGroup? - var isAllOKInGroup = true - - if enableGCDGroup { - group = DispatchGroup() - } - - var tempPhotos = photos - // ⚠️ - // let checkImageNeedOfBucket = needCheckImage(for: bucket) - - for photo in photos { - if let tempString = photo.remoteImageUrlString, tempString.count > 0 { - tempPhotos.removeAll { $0 === photo } - removeObjFromUploadingItems(by: photo) - photo.mm_uploadDone(tempString) - continue - } - - guard photo.image != nil || photo.imageData != nil else { - dlog("❌❌❌: 错误路径,图片应不能为空【CloudStorage】") - continue - } - - if enableGCDGroup { - group?.enter() - } - - if photo.imageData == nil { - photo.imageData = convertImageToData(photo.image!, suffix: photo.suffixEnum) - } - - let sizeInMB = Float(photo.imageData!.count) / (1000.0 * 1000.0) - if sizeInMB > 10 { - photo.setupPhotoOversizeLimmit() - isAllOKInGroup = false - DispatchQueue.main.async { - let message = photo.suffixEnum == .gif ? "Photo must be .JPG, .JPEG, .GIF or .PNG and cannot exceed 10MB" : "Photo must be .JPG, .JPEG or .PNG and cannot exceed 10MB" - photo.setupErrorMsg(message) - // Assuming Hud is a utility for showing alerts/toasts - // Unknown: Hud ShowPureTip implementation - dlog(message) // Placeholder for Hud.ShowPureTip - photo.mm_uploadFailed() - } - if enableGCDGroup { - group?.leave() - } - continue - } - -// && checkImageNeedOfBucket - let isCheckImageInCallback = block != nil && photo.isAutoCheckImage - if isCheckImageInCallback { - photo.banCheckImageInDelegateMethod = true - } - - let suffixType = photo.imageData != nil ? photo.suffixEnum : .jpeg - - requestAuthToken(bucket, suffix: suffixType) { [weak self] result, authData, utility in - guard let self = self, let s3AuthData = authData, let s3Utility = utility else { return } - if result { - self.s3Upload(s3Utility, photoM: photo, authData: s3AuthData) { success in - photo.isUploading = false - if success { - if isCheckImageInCallback { - photo.checkImageOK { result2 in - if !result2 { // 鉴定失败❌ - isAllOKInGroup = false - } - if enableGCDGroup { - group?.leave() - } - } - } else { - // 不需要鉴黄,直接返回 - if enableGCDGroup { - group?.leave() - } - } - } else { - // 上传失败❌ - isAllOKInGroup = false - if enableGCDGroup { - group?.leave() - } - } - } - } else { - // 获取token失败❌ - isAllOKInGroup = false - if enableGCDGroup { - group?.leave() - } - DispatchQueue.main.async { - photo.mm_uploadFailed() - } - } - } - } - - if enableGCDGroup, let validGroup = group { -// DispatchQueue.main.async(group: group) { - validGroup.notify(queue: .main) { - block?(isAllOKInGroup) - } - } - } - - private func s3Upload(_ utility: AWSS3TransferUtility, photoM im: UploadPhotoM, authData: S3AuthData, result: @escaping (Bool) -> Void) { - guard !authData.path.isEmpty else { - DispatchQueue.main.async { - dlog("☁️❌authData.path is empty") - im.mm_uploadFailed() - result(false) - } - return - } - - if LTNetworkManage.ltManage.reachable == false { // Placeholder for [EGApiManager shared].status == .notReachable - DispatchQueue.main.async { - im.mm_uploadFailed() - result(false) - } - return - } - - im.s3AuthData = authData - im.utility = utility - - let key = authData.path - dlog("☁️uplading key: \(key)") - - let expression = AWSS3TransferUtilityUploadExpression() - expression.progressBlock = { _, progress in - dlog("🚀 \(key) taskprogress: \(progress)") - } - expression.setValue("temp=1", forRequestHeader: "x-amz-tagging") - - guard let imageData = convertImageToData(im.image!, suffix: im.suffixEnum) ?? im.imageData else { return } - let contentType = im.contentType() - - utility.uploadData(imageData, bucket: authData.bucket, key: key, contentType: contentType, expression: expression) { task, error in - self.removeObjFromUploadingItems(by: im) - if let error = error { - dlog("❌S3上传任务失败: \(error.localizedDescription)") - DispatchQueue.main.async { - im.mm_uploadFailed() - result(false) - } - return - } - - switch task.status { - case .completed: - im.remoteFullPath = authData.urlPath - im.remoteImageUrlString = key - dlog("💹 s3 image uploaded full: \(im.remoteFullPath ?? "")") - DispatchQueue.main.async { - im.mm_uploadDone(key) - result(true) - } - case .error, .cancelled: - DispatchQueue.main.async { - im.mm_uploadFailed() - result(false) - } - default: - assert(false, "上传异常分支、需要查查原因") - } - } - } - - private func s3UploadModel(_ utility: AWSS3TransferUtility, object model: UploadModel, auth: S3AuthData, result: @escaping (Bool) -> Void) { - guard !auth.path.isEmpty, let fileData = model.fileData else { - DispatchQueue.main.async { - model.mm_uploadFailed() - } - result(false) - return - } - - // Assuming EGApiManager is a network status manager - // Unknown: EGApiManager shared status implementation - if false { // Placeholder for [EGApiManager shared].status == .notReachable - DispatchQueue.main.async { - model.mm_uploadFailed() - result(false) - } - return - } - - model.s3AuthData = auth - model.utility = utility - - var key = auth.path - /// dev/main/sound/0/* - if key.hasSuffix("/*") { - // 移除最后的 "/*" - key.removeLast(2) - // 拼接时间戳 + .mp3 - key += "/\(model.addThisItemTimeStamp).mp3" - } - - var urlPath = auth.urlPath ?? "" - if urlPath.hasSuffix("/*") { - // 移除最后的 "/*" - urlPath.removeLast(2) - // 拼接时间戳 + .mp3 - urlPath += "/\(model.addThisItemTimeStamp).mp3" - } - dlog("☁️uplading file key: \(key) full: \(urlPath)") - - let expression = AWSS3TransferUtilityUploadExpression() - expression.progressBlock = { _, progress in - dlog("🚀 \(key) file taskprogress: \(progress)") - } - expression.setValue("temp=1", forRequestHeader: "x-amz-tagging") - - let contentType = model.contentType() - - utility.uploadData(fileData, bucket: auth.bucket, key: key, contentType: contentType, expression: expression) { task, error in - self.uploadAudiosItems.removeAll { $0 === model } - if let error = error { - dlog("❌S3上传任务失败: \(error.localizedDescription)") - result(false) - return - } - - switch task.status { - case .completed: - model.remoteFullPath = urlPath // auth.urlPath - model.remoteImageUrlString = key - dlog("💹 s3 file uploaded full: \(model.remoteFullPath ?? "")") - DispatchQueue.main.async { - model.mm_uploadDone(key) - result(true) - } - case .error, .cancelled: - DispatchQueue.main.async { - model.mm_uploadFailed() - result(false) - } - default: - assert(false, "上传异常分支、需要查查原因") - } - } - } - - private func requestAuthToken(_ bucket: BucketS3Enum, suffix: SuffixS3Enum, callback: @escaping (Bool, S3AuthData?, AWSS3TransferUtility?) -> Void) { - OssProvider.request(.getS3Token(bucketNameEnum: bucket, suffix: suffix), modelType: S3AuthData.self) {[weak self] result in - switch result { - case let .success(success): - if let authData = success { - let utility = self?.generateTransferUtility(bucket, authData: authData) - callback(true, authData, utility) - - if bucket == .SOUND_PATH{ - self?.authAudioData = success - } - } else { - callback(false, nil, nil) - } - case .failure: - callback(false, nil, nil) - } - } - } - - private func generateTransferUtility(_ bucket: BucketS3Enum, authData: S3AuthData) -> AWSS3TransferUtility? { - guard let accessKeyId = authData.accessKeyId, !accessKeyId.isEmpty, - let accessKeySecret = authData.accessKeySecret, !accessKeySecret.isEmpty, - let securityToken = authData.securityToken, !securityToken.isEmpty else { - assert(false, "invalid token") - return nil - } - - /// Region(⚠️String) from server - // let region = authData.region - - let provider = AWSBasicSessionCredentialsProvider(accessKey: accessKeyId, secretKey: accessKeySecret, sessionToken: securityToken) - // ⚠️(Integer)支持哪些region,需要转化。目前强制 .USWest2 - let regionType: AWSRegionType = .USWest2; - guard let config = AWSServiceConfiguration(region: regionType, credentialsProvider: provider) else { - assert(false, "Invalid AWSServiceConfiguration") - return nil - } - config.timeoutIntervalForRequest = 30 - config.maxRetryCount = 1 - - let key = authData.fileName - AWSS3TransferUtility.remove(forKey: key) // AWSS3TransferUtility.removeS3TransferUtility(forKey: key) - AWSS3TransferUtility.register(with: config, forKey: key) - - let utility = AWSS3TransferUtility.s3TransferUtility(forKey: key) // AWSS3TransferUtility(forKey: key) - if utility == nil { - assert(false, "invalid utility") - } - - switch bucket{ - case .SOUND_PATH: - audioTransferUtility = utility - default: - break - } - - return utility - } - - private func judgeTokenValid(_ lastRequestDate: Date?) -> Bool { - guard let date = lastRequestDate else { - return false - } - let expireDate = date + 59.minutes - - if expireDate.milliStamp > Date().milliStamp { - return true - } - return false - } - - private func removeObjFromUploadingItems(by obj: UploadPhotoM) { - uploadAlbumItems.removeAll { $0 === obj } - uploadHeadImagesItems.removeAll { $0 === obj } - uploadRoleItems.removeAll { $0 === obj } - uploadAudiosItems.removeAll { $0 === obj } - } - - private func cancelTask(for item: T) { - // Unknown: AWSTask and AWSS3TransferUtilityUploadTask implementation - if let utility = item.utility { - // Placeholder for task cancellation - dlog("🈲🈲, upload task canceled : \(utility)") - DispatchQueue.main.async { - item.mm_uploadFailed() - } - } - } - - // MARK: - Notification Handling - - @objc private func receiveNetStatusChanged(_ notification: Notification) { - let status = LTNetworkManage.ltManage.status - if status == .notReachable { - cancelTasks(for: .ALBUM) - cancelTasks(for: .HEAD_IMAGE) - cancelTasks(for: .ROLE) - cancelTasks(for: .SOUND) - cancelTasks(for: .IM_IMG) - } - } - - // MARK: - Helper Methods - - private func convertImageToData(_ image: UIImage, suffix: SuffixS3Enum) -> Data? { - // Unknown: SDWebImage or UIImage conversion implementation - switch suffix { - case .gif: - assert(false, "imageData应该不为空") - return nil - // return image.sd_imageData(as: .gif) // Placeholder - case .png: - return image.pngData() - default: - return image.jpegData(compressionQuality: 0.8) - } - } -} diff --git a/crush/Crush/Src/Utils/S3/CommentImageModel.swift b/crush/Crush/Src/Utils/S3/CommentImageModel.swift deleted file mode 100644 index a80ea70..0000000 --- a/crush/Crush/Src/Utils/S3/CommentImageModel.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// CommentImageModel.swift -// Crush -// -// Created by Leon on 2025/7/21. -// - -import Foundation - -class CommentImageModel { - var uploadUrl: String? - var image: UIImage? - var isReport: Bool = false - var isCoach: Bool = false - var imageUploadComplete: ((Bool, UploadPhotoM) -> Void)? - - @discardableResult - func startUpload(with image: UIImage, checkImage: Bool = false) -> UploadPhotoM { - self.image = image - - if isReport { - let model = UploadPhotoM() - model.image = image - model.imageSize = image.pixelSize - model.isAutoCheckImage = checkImage - // Unknown: [NSDate date] timestamp implementation - model.addThisItemTimeStamp = Int(Date().timeIntervalSince1970) // Placeholder for timestamp - - CloudStorage.shared.s3BatchAddPhotos([model], bucket: .IM_IMG) { [weak self] result in - guard let self = self else { return } - if result { - self.uploadUrl = model.remoteFullPath - } - self.imageUploadComplete?(result, model) - } - return model - }else { - let model = UploadPhotoM() - model.image = image - model.imageSize = image.pixelSize - model.isAutoCheckImage = checkImage - // Unknown: [NSDate date] timestamp implementation - model.addThisItemTimeStamp = Int(Date().timeIntervalSince1970) // Placeholder for timestamp - CloudStorage.shared.s3BatchAddPhotos([model], bucket: .IM_IMG) { [weak self] result in - guard let self = self else { return } - if result { - self.uploadUrl = model.remoteFullPath - } - self.imageUploadComplete?(result, model) - } - return model - } - } -} diff --git a/crush/Crush/Src/Utils/TextHighlightHelper.swift b/crush/Crush/Src/Utils/TextHighlightHelper.swift deleted file mode 100644 index 000dec6..0000000 --- a/crush/Crush/Src/Utils/TextHighlightHelper.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// TextHighlightHelper.swift -// Crush -// -// Created by Leon on 2025/1/15. -// - -import UIKit - -/// 此方法:高亮部分是背景色不同,不是文字颜色 -class TextHighlightHelper { - - /// 为文本中的搜索关键词添加高亮样式 - /// - Parameters: - /// - text: 原始文本 - /// - searchText: 搜索关键词 - /// - highlightColor: 高亮颜色 - /// - Returns: 带有高亮样式的NSAttributedString - static func highlightText(_ text: String, searchText: String, highlightColor: UIColor = UIColor.c.cpvn) -> NSAttributedString { - guard !searchText.isEmpty else { - return NSAttributedString(string: text) - } - - let attributedString = NSMutableAttributedString(string: text) - let range = NSRange(location: 0, length: text.count) - - // 使用正则表达式进行不区分大小写的搜索 - do { - let regex = try NSRegularExpression(pattern: NSRegularExpression.escapedPattern(for: searchText), options: .caseInsensitive) - let matches = regex.matches(in: text, options: [], range: range) - - // 为匹配的文本添加高亮样式 - for match in matches { - // backgroundColor - attributedString.addAttribute(.foregroundColor, value: highlightColor, range: match.range) - } - } catch { - // 如果正则表达式失败,使用简单的字符串搜索 - let lowercasedText = text.lowercased() - let lowercasedSearchText = searchText.lowercased() - - var searchRange = lowercasedText.startIndex.. Void)?) { - if let appVer = appVersion { - block?(appVer) - return - } - -// CommonProvider.request(.appVersionGet, modelType: AppVersion.self, autoShowErrMsg: false) { [weak self] result in -// switch result { -// case let .success(model): -// self?.appVersion = model -// block?(model) -// break -// case .failure: -// break -// } -// } - } - - public func findLocaliezedNameBy(code: String) -> String? { -// for per in langNameCodes { -// if per.langCode == code { -// return per.codeName -// } -// } - - return nil - } - - static func syncSystemLanguage() { - } - - /// 多语言适配 - static func syncAppLanuage(lan: Languages, completion: ((_ result: Bool) -> Void)?) { -// MeProvider.request(.updateLanguage(lang: lan.rawValue), modelType: String.self, autoShowErrMsg: false) { result in -// switch result { -// case .success: -// completion?(true) -// break -// case .failure: -// completion?(false) -// break -// } -// } - } - - /// 通过regionCode来决定国家显示名称 - static func getLocalizedCountryName(localeIdentifier: String?) -> String { - guard let identi = localeIdentifier else { return "" } - - var regionCode = "en" - - // 获取设备的国家Code - if let currentRegionCode = Locale.current.regionCode { - regionCode = currentRegionCode - } - - let locale = Locale(identifier: regionCode) - if let localisedCountryName = locale.localizedString(forRegionCode: identi) { - return localisedCountryName - } else { - dlog("⚠️无法localize国家:\(identi)") - return "" - } - } - - static func getLocalizedCountryNameByPreferLan(localeIdentifier: String?) -> String { - guard let identi = localeIdentifier else { return "" } - - guard let lan = Languages.preferedLans.first else { return "" } - let locale = Locale(identifier: Languages.regionCode(lan: lan)) - - if let localisedCountryName = locale.localizedString(forRegionCode: identi) { - return localisedCountryName - } else { - dlog("Languages Prefer lan 's locale country name get failed ❌:\(identi) 采用备选方案根据Locale.current来获取") - return getLocalizedCountryName(localeIdentifier: localeIdentifier) - } - } - - // MARK: - helper. -} - -extension CLTool { - static func idfaSetupReport() { - if #available(iOS 14, *) { - ATTrackingManager.requestTrackingAuthorization { status in - switch status { - case .denied: - debugPrint("【IDFA】用户拒绝") - break - case .authorized: - debugPrint("【IDFA】用户允许:❇️IDFA: \(ASIdentifierManager.shared().advertisingIdentifier.uuidString)❇️") - CLTool.tryReportIDFA() - break - case .notDetermined: - debugPrint("【IDFA】用户没有选择") - default: - break - } - } - } else { - // iOS13及之前版本,继续用以前的方式 - if ASIdentifierManager.shared().isAdvertisingTrackingEnabled { - // debugPrint("可以获取:\(ASIdentifierManager.shared().advertisingIdentifier.uuidString)") - CLTool.tryReportIDFA() - } else { - debugPrint("【IDFA】用户未打开IDFA开关") - } - } - } - - fileprivate static func tryReportIDFA() { - let str = ASIdentifierManager.shared().advertisingIdentifier.uuidString - if str.contains("0000-0000-0000") || str.count == 0 { - return - } - - if let isSubmmited = AppCache.fetchCache(key: CacheKey.boolIdfaSubmmited.rawValue, type: Bool.self), isSubmmited == true { - return - } - -// ComplexProvider.request(.reportIDFA(str: str), modelType: String.self, autoShowErrMsg: false) { result in -// switch result { -// case let .success(model): -// dlog(model) -// AppCache.cache(key: CacheKey.boolIdfaSubmmited.rawValue, value: true) -// break -// case .failure: -// break -// } -// } - } -} - -// MARK: Sandbox - -extension CLTool{ - static func m4aFileToBase64(fileURL: URL?) -> String? { - guard let fileURL = fileURL else { - dlog("文件 URL 为空") - return nil - } - - do { - let data = try Data(contentsOf: fileURL) - let base64String = data.base64EncodedString() - return base64String - } catch { - dlog("读取或转换失败: \(error)") - return nil - } - } -} diff --git a/crush/Crush/Src/Utils/UserCore.swift b/crush/Crush/Src/Utils/UserCore.swift deleted file mode 100644 index 6cc1e4c..0000000 --- a/crush/Crush/Src/Utils/UserCore.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// UserCore.swift -// Crush -// -// Created by Leon on 2025/7/12. -// - -import Foundation -import NIMSDK - -typealias UserInfoUpdatedClosure = ((_ result: Bool) -> Void) - -let UDTokenKey = "UDTokenKey" -let UDUserInfoKey = "UDUserInfoKey" - -@objcMembers class UserCore: NSObject { - public static let shared = UserCore() - - @Published private(set) var user: User? - private(set) var token: String = "" - - override init() { - super.init() - - NotificationCenter.default.addObserver(self, selector: #selector(notiLoginSuccess), name: AppNotificationName.userLoginSuccess.notificationName, object: nil) - } - - // MARK: - public - - public func isLogin() -> Bool { -// #warning("test") -// return true - return token.count > 0 - } - - public func isSelf(id: Int?) -> Bool{ - guard let uid = user?.userId, let fromUid = id else { return false } - return uid == fromUid - } - - public func refreshUserInfo(block: UserInfoUpdatedClosure?) { - let tokenNow = token - guard tokenNow.count > 0 else { - block?(false) - dlog("⚠️ token is nil, cannot load user info") - return - } - - UserProvider.request(.userInfoSelfGet, modelType: User.self) {[weak self] result in - switch result { - case let .success(model): - if model != nil { - self?.updateLocalUser(userNew: model) - block?(true) - NotificationCenter.post(name: .userInfoUpdated, object: nil, userInfo: nil) - } else { - block?(false) - } - - case let .failure(error): - dlog("refresh user info error: \(error)") - block?(false) - print(error) - } - } - } - - @discardableResult - public func checkUserLoginIfNotPushUserToLogin() -> Bool { - if isLogin() { - return true - } - - NotificationCenter.post(name: .presentSignInVc, object: nil, userInfo: nil) - return false - } - - // MARK: - Core - public func autoLoginTry() { - if let dict: String = UserDefaults.standard.object(forKey: UDUserInfoKey) as? String { - dlog("👩👩 userInfo restore from UD: \(dict)") - if let userNow = CodableHelper.decode(User.self, from: dict){//User.deserialize(from: dict). - user = userNow - } - } - - if let tokenUD = UserDefaults.standard.string(forKey: UDTokenKey), tokenUD.count > 0 { - dlog("🔥 user token: \r\(tokenUD)") - token = tokenUD - - refreshUserInfo {[weak self] result in - if result { - dlog("👩自动用户登录OK \(tokenUD)") - if let cpUserInfo = self?.user?.cpUserInfo, cpUserInfo == true{ - AppRouter.goPersonalInformationFill(tokenUD) - } - } else { - dlog("👩自动用户登录 Error") - } - } - - } else { - dlog("👩未登录") - } - } - - @discardableResult - public func logout() -> Bool { - if isLogin() == false { - return false - } - - IMManager.shared.logoutIM() - - UIApplication.shared.applicationIconBadgeNumber = 0 - emptyToken() - UserDefaults.standard.removeObject(forKey: UDUserInfoKey) - UserDefaults.standard.synchronize() - - user = User() - - // #warning("登录之前的请求继续请求?") - - NotificationCenter.post(name: .userInfoUpdated, object: nil, userInfo: nil) - NotificationCenter.post(name: .userLogout, object: nil, userInfo: nil) - - // 三方登出 - return true - } - - // MARK: - Helper - - - public func updateToken(tokenNew: String) { - if tokenNew.count == 0 { - assert(false) - return - } - UserDefaults.standard.set(tokenNew, forKey: UDTokenKey) - token = tokenNew - } - - func updateLocalUser(userNew: User?) { - user = userNew - if let u = userNew, let validToken = u.token, validToken.isEmpty{ - token = validToken - } - if let jsonString = CodableHelper.encodeToJSONString(userNew){ - dlog("Save to UD user jsonstring: \(jsonString)") - UserDefaults.standard.set(jsonString, forKey: UDUserInfoKey) - } - NotificationCenter.post(name: .userInfoUpdated, object: nil, userInfo: nil) - } - - private func emptyToken() { - token = "" - UserDefaults.standard.removeObject(forKey: UDTokenKey) - UserDefaults.standard.synchronize() - } - - func isSelfByUid(uid: Int?) -> Bool{ - guard let userId = uid, let userLogged = self.user else {return false} - if userLogged.userId == userId{ - return true - } - return false - } - - // MARK: - noti - - func notiLoginSuccess() { - // 一些只在登录后需要上传的内容:如地理信息,暂无 - // ... - } -} diff --git a/crush/Crush/Src/ViewModel/PhotosViewModel.swift b/crush/Crush/Src/ViewModel/PhotosViewModel.swift deleted file mode 100644 index f70bdd1..0000000 --- a/crush/Crush/Src/ViewModel/PhotosViewModel.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// PhotosViewModel.swift -// Crush -// -// Created by Leon on 2025/9/30. -// - -class PhotosViewModel{ - static let shared = PhotosViewModel() - - @Published var album: AlbumPhotoItem? -} diff --git a/crush/Crush/Src/ViewModel/UserOperator.swift b/crush/Crush/Src/ViewModel/UserOperator.swift deleted file mode 100644 index ee206cf..0000000 --- a/crush/Crush/Src/ViewModel/UserOperator.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// UserOperator.swift -// Crush -// -// Created by Leon on 2025/8/11. -// - - -struct UserOperator{ - private init(){} - - /// uid: 表示aiId,如果不是则不用传,排除自己的userId - static func checkname(name:String?, uid : Int? = nil, block:((_ nicknameOK: Bool) -> Void)?){ - let validname = name ?? "" - var exUserId : Int? = nil - if let paramsUid = uid{ - exUserId = paramsUid - }else if let userId = UserCore.shared.user?.userId{ - exUserId = userId - } - - UserProvider.request(.userNicknameCheck(nickname: validname, exUserId: exUserId), modelType: Bool?.self) {result in - switch result { - case .success(let success): - if let checkResult = success, checkResult == false{ - block?(true) - }else{ - Hud.toast(str: "该昵称已存在") - block?(false) - } - case .failure: - block?(false) - break - } - } - } -} diff --git a/crush/Crush/ViewController.swift b/crush/Crush/ViewController.swift deleted file mode 100644 index f787933..0000000 --- a/crush/Crush/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// Crush -// -// Created by lyu dong on 2025/7/8. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/crush/CrushTests/CLSystemTokenTests.swift b/crush/CrushTests/CLSystemTokenTests.swift deleted file mode 100644 index 18fd2aa..0000000 --- a/crush/CrushTests/CLSystemTokenTests.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// CrushTests.swift -// CrushTests -// -// Created by Leon on 2025/7/11. -// - -import XCTest -@testable import Crush - -class CLSystemTokenTests: XCTestCase { - - override func setUp() { - super.setUp() - - } - - override func tearDown() { - super.tearDown() - - } - - // 测试有效颜色的获取 - func testColorForValidToken() { - let expectedColor = UIColor(red: 1.0, green: 0.925, blue: 0.871, alpha: 1.0) // #FFECDE - let color = CLSystemToken.color(token: .cph) - - XCTAssertNotNil(color, "Color should not be nil for valid token") - -// if let color = color { -// XCTAssertEqual(color, expectedColor, accuracy: 0.01, "Color should match expected RGB values") -// } - } - - // 测试带透明度的颜色获取 - func testColorWithAlpha() { - // 假设 skin.json 中有一个键返回 ["#FFECDE", "0.5"] - let token = CLEnumSystemToken(rawValue: "color.orange.with.alpha") - let expectedColor = UIColor(red: 1.0, green: 0.925, blue: 0.871, alpha: 0.5) - - // 模拟 EPUI 返回带透明度的值 - let mockValues = ["#FFECDE", "0.5"] - // 假设 EPUI.getTokensByKey 返回 mockValues(需要 mock 或修改 skin.json) - let color = EPBaseObject.subGetColorByTokenValues(mockValues) - - XCTAssertNotNil(color, "Color with alpha should not be nil") - - if let color = color { - - //XCTAssertEqual(color, expectedColor, accuracy: 0.01, "Color with alpha should match expected RGBA values") - //XCTAssertEqual(color, expectedColor, accuracy: 0.01, "xxx") - } - } - - // 测试无效 token -// func testColorForInvalidToken() { -// let token = CLEnumSystemToken(rawValue: "invalid.color")! -// let color = CLSystemToken.color(token: token) -// -// XCTAssertNil(color, "Color should be nil for invalid token") -// } - - // 测试深色模式颜色获取 -// func testDarkColorForValidToken() { -// let token = CLEnumSystemToken(rawValue: "color.orange.90")! // 深色模式示例 -// let expectedColor = UIColor(red: 0.302, green: 0.078, blue: 0.0, alpha: 1.0) // #4D1400 -// let color = CLSystemToken.darkColor(token: token) -// -// XCTAssertNotNil(color, "Dark color should not be nil for valid token") -// -//// if let color = color { -//// XCTAssertEqual(color, expectedColor, accuracy: 0.01, "Dark color should match expected RGB values") -//// } -// } - - // 测试空值情况 -// func testColorForEmptyValues() { -// let emptyValues: [String] = [] -// let color = EPBaseObject.subGetColorByTokenValues(emptyValues) -// -// XCTAssertNil(color, "Color should be nil for empty values") -// } -} - -// 扩展 UIColor 以支持比较 -extension UIColor { - func isEqual(to color: UIColor, accuracy: CGFloat) -> Bool { - var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0 - var r2: CGFloat = 0, g2: CGFloat = 0, b2: CGFloat = 0, a2: CGFloat = 0 - - self.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) - color.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) - - return abs(r1 - r2) <= accuracy && - abs(g1 - g2) <= accuracy && - abs(b1 - b2) <= accuracy && - abs(a1 - a2) <= accuracy - } -} diff --git a/crush/Podfile b/crush/Podfile deleted file mode 100644 index 04b1b71..0000000 --- a/crush/Podfile +++ /dev/null @@ -1,60 +0,0 @@ -platform :ios, '15.0' -use_frameworks! -inhibit_all_warnings! - -#install! 'cocoapods', -# :generate_multiple_pod_projects => true, -# :incremental_installation => true -# -source 'https://github.com/byteplus-sdk/byteplus-specs.git' -source 'https://cdn.cocoapods.org/' -target 'Crush' do - - # Base - pod 'Moya' - pod 'SnapKit' - pod 'R.swift' - pod 'Cache' - pod 'DateToolsSwift' - pod 'SwiftDate' - pod 'Kingfisher' - pod 'APNGKit', '~> 2.0' - - # View - pod 'ActiveLabel' - pod 'JXSegmentedView' - pod 'JXPagingView/Paging' - pod 'lottie-ios' - pod 'TZImagePickerController', :git => 'https://github.com/OfficialEPal/TZImagePickerController.git', :branch => "Crush" - pod 'SwipeCellKit' - - # Helper - pod 'SwiftyAttributes' - pod 'IQKeyboardManagerSwift' - - # Util & third - pod 'NIMSDK_LITE' - pod 'AWSMobileClient' - pod 'AWSS3' -# pod 'URLNavigator' - pod 'BytePlusRTC', '~> 3.58.1' - - # OC - pod 'SDWebImage' - pod 'Masonry' - - pod 'UICKeyChainStore' - pod 'MJExtension' - pod 'MJRefresh' - - # Debug - pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug'] - - post_install do |installer| - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' - end - end - end -end diff --git a/crush/Podfile.lock b/crush/Podfile.lock deleted file mode 100644 index 9fd32a2..0000000 --- a/crush/Podfile.lock +++ /dev/null @@ -1,231 +0,0 @@ -PODS: - - ActiveLabel (1.1.0) - - Alamofire (5.10.2) - - APNGKit (2.3.0): - - Delegate (~> 1.3) - - AWSAuthCore (2.41.0): - - AWSCore (= 2.41.0) - - AWSCognitoIdentityProvider (2.41.0): - - AWSCognitoIdentityProviderASF (= 2.41.0) - - AWSCore (= 2.41.0) - - AWSCognitoIdentityProviderASF (2.41.0): - - AWSCore (= 2.41.0) - - AWSCore (2.41.0) - - AWSMobileClient (2.41.0): - - AWSAuthCore (= 2.41.0) - - AWSCognitoIdentityProvider (= 2.41.0) - - AWSCognitoIdentityProviderASF (= 2.41.0) - - AWSCore (= 2.41.0) - - AWSS3 (2.41.0): - - AWSCore (= 2.41.0) - - BytePlusRTC (3.58.1.31100): - - BytePlusRTC/Core (= 3.58.1.31100) - - BytePlusRTC/RealXBase (= 3.58.1.31100) - - BytePlusRTC/RTCFFmpeg (= 3.58.1.31100) - - BytePlusRTC/Core (3.58.1.31100) - - BytePlusRTC/RealXBase (3.58.1.31100) - - BytePlusRTC/RTCFFmpeg (3.58.1.31100) - - Cache (6.0.0) - - DateToolsSwift (5.0.0) - - Delegate (1.3.0) - - IQKeyboardCore (1.0.5) - - IQKeyboardManagerSwift (8.0.1): - - IQKeyboardManagerSwift/Appearance (= 8.0.1) - - IQKeyboardManagerSwift/Core (= 8.0.1) - - IQKeyboardManagerSwift/IQKeyboardReturnManager (= 8.0.1) - - IQKeyboardManagerSwift/IQKeyboardToolbarManager (= 8.0.1) - - IQKeyboardManagerSwift/IQTextView (= 8.0.1) - - IQKeyboardManagerSwift/Resign (= 8.0.1) - - IQKeyboardManagerSwift/Appearance (8.0.1): - - IQKeyboardManagerSwift/Core - - IQKeyboardManagerSwift/Core (8.0.1): - - IQKeyboardNotification - - IQTextInputViewNotification - - IQKeyboardManagerSwift/IQKeyboardReturnManager (8.0.1): - - IQKeyboardReturnManager - - IQKeyboardManagerSwift/IQKeyboardToolbarManager (8.0.1): - - IQKeyboardManagerSwift/Core - - IQKeyboardToolbarManager - - IQKeyboardManagerSwift/IQTextView (8.0.1): - - IQTextView - - IQKeyboardManagerSwift/Resign (8.0.1): - - IQKeyboardManagerSwift/Core - - IQKeyboardNotification (1.0.3) - - IQKeyboardReturnManager (1.0.4): - - IQKeyboardCore (= 1.0.5) - - IQKeyboardToolbar (1.1.1): - - IQKeyboardCore - - IQKeyboardToolbar/Core (= 1.1.1) - - IQKeyboardToolbar/Core (1.1.1): - - IQKeyboardCore - - IQKeyboardToolbar/Placeholderable - - IQKeyboardToolbar/Placeholderable (1.1.1): - - IQKeyboardCore - - IQKeyboardToolbarManager (1.1.3): - - IQKeyboardToolbar - - IQTextInputViewNotification - - IQTextInputViewNotification (1.0.8): - - IQKeyboardCore - - IQTextView (1.0.5): - - IQKeyboardToolbar/Placeholderable - - JXPagingView/Paging (2.1.3) - - JXSegmentedView (1.4.1) - - Kingfisher (8.4.0) - - LookinServer/Core (1.2.8) - - LookinServer/Swift (1.2.8): - - LookinServer/Core - - lottie-ios (4.5.2) - - Masonry (1.1.0) - - MJExtension (3.4.2) - - MJRefresh (3.7.9) - - Moya (15.0.0): - - Moya/Core (= 15.0.0) - - Moya/Core (15.0.0): - - Alamofire (~> 5.0) - - NIMSDK_LITE (10.9.43): - - NIMSDK_LITE/NOS (= 10.9.43) - - YXArtemis_XCFramework - - NIMSDK_LITE/NOS (10.9.43): - - YXArtemis_XCFramework - - R.swift (7.8.0) - - SDWebImage (5.21.1): - - SDWebImage/Core (= 5.21.1) - - SDWebImage/Core (5.21.1) - - SnapKit (5.7.1) - - SwiftDate (7.0.0) - - SwiftyAttributes (5.4.0) - - SwipeCellKit (2.7.1) - - TZImagePickerController (3.8.9): - - TZImagePickerController/Basic (= 3.8.9) - - TZImagePickerController/Location (= 3.8.9) - - TZImagePickerController/Basic (3.8.9) - - TZImagePickerController/Location (3.8.9) - - UICKeyChainStore (2.2.1) - - YXArtemis_XCFramework (1.1.6) - -DEPENDENCIES: - - ActiveLabel - - APNGKit (~> 2.0) - - AWSMobileClient - - AWSS3 - - BytePlusRTC (~> 3.58.1) - - Cache - - DateToolsSwift - - IQKeyboardManagerSwift - - JXPagingView/Paging - - JXSegmentedView - - Kingfisher - - LookinServer/Swift - - lottie-ios - - Masonry - - MJExtension - - MJRefresh - - Moya - - NIMSDK_LITE - - R.swift - - SDWebImage - - SnapKit - - SwiftDate - - SwiftyAttributes - - SwipeCellKit - - TZImagePickerController (from `https://github.com/OfficialEPal/TZImagePickerController.git`, branch `Crush`) - - UICKeyChainStore - -SPEC REPOS: - https://github.com/byteplus-sdk/byteplus-specs.git: - - BytePlusRTC - trunk: - - ActiveLabel - - Alamofire - - APNGKit - - AWSAuthCore - - AWSCognitoIdentityProvider - - AWSCognitoIdentityProviderASF - - AWSCore - - AWSMobileClient - - AWSS3 - - Cache - - DateToolsSwift - - Delegate - - IQKeyboardCore - - IQKeyboardManagerSwift - - IQKeyboardNotification - - IQKeyboardReturnManager - - IQKeyboardToolbar - - IQKeyboardToolbarManager - - IQTextInputViewNotification - - IQTextView - - JXPagingView - - JXSegmentedView - - Kingfisher - - LookinServer - - lottie-ios - - Masonry - - MJExtension - - MJRefresh - - Moya - - NIMSDK_LITE - - R.swift - - SDWebImage - - SnapKit - - SwiftDate - - SwiftyAttributes - - SwipeCellKit - - UICKeyChainStore - - YXArtemis_XCFramework - -EXTERNAL SOURCES: - TZImagePickerController: - :branch: Crush - :git: https://github.com/OfficialEPal/TZImagePickerController.git - -CHECKOUT OPTIONS: - TZImagePickerController: - :commit: 5deb14dfa3132b76e36ca85a511d111685ec3f9f - :git: https://github.com/OfficialEPal/TZImagePickerController.git - -SPEC CHECKSUMS: - ActiveLabel: 5e3f4de79a1952d4604b845a0610d4776e4b82b3 - Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 - APNGKit: eb7e111277527cfd47636f797c9c8e7aab5d9601 - AWSAuthCore: d02cf2eec71144b65e7985bd7142478f09b7d519 - AWSCognitoIdentityProvider: b7e2471bec75137ce392b9a8c60f3cee465c61bd - AWSCognitoIdentityProviderASF: d26fedd80485c2f0f308741e02c4a9093921ad22 - AWSCore: ee7594bda3132cde8666328bf9a09d8770c1c100 - AWSMobileClient: 41f55548030e3898f51db407c727b5e133214222 - AWSS3: 50da7408a26c1c2886d02968807f2a0b6bf936ca - BytePlusRTC: a6c53912a85317661592b0bb20d3425e998db147 - Cache: 4ca7e00363fca5455f26534e5607634c820ffc2d - DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 - Delegate: 0ff4467868095239ff578ab531efd8af46e62881 - IQKeyboardCore: 28c8bf3bcd8ba5aa1570b318cbc4da94b861711e - IQKeyboardManagerSwift: 835fc9c6e4732398113406d84900ad2e8f141218 - IQKeyboardNotification: d7382c4466c5a5adef92c7452ebf861b36050088 - IQKeyboardReturnManager: 972be48528ce9e7508ab3ab15ac7efac803f17f5 - IQKeyboardToolbar: d4bdccfb78324aec2f3920659c77bb89acd33312 - IQKeyboardToolbarManager: 6c693c8478d6327a7ef2107528d29698b3514dbb - IQTextInputViewNotification: f5e954d8881fd9808b744e49e024cc0d4bcfe572 - IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7 - JXPagingView: afdd2e9af09c90160dd232b970d603cc6e7ddd0e - JXSegmentedView: cd73555ce2134d1656db2cb383ba9c2f36fb5078 - Kingfisher: b14cc47bbfa7a3c150dd12962ee9c86338545629 - LookinServer: 1b2b61c6402ae29fa22182d48f5cd067b4e99e80 - lottie-ios: 96784afc26ea031d3e2b6cae342a4b8915072489 - Masonry: 678fab65091a9290e40e2832a55e7ab731aad201 - MJExtension: e97d164cb411aa9795cf576093a1fa208b4a8dd8 - MJRefresh: ff9e531227924c84ce459338414550a05d2aea78 - Moya: 138f0573e53411fb3dc17016add0b748dfbd78ee - NIMSDK_LITE: 7fc7250c0acef83553e94cdd57fccdca29eebddf - R.swift: f573269ca45b2ab066c082e363dd4c2b297b0d71 - SDWebImage: f29024626962457f3470184232766516dee8dfea - SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a - SwiftDate: bbc26e26fc8c0c33fbee8c140c5e8a68293a148a - SwiftyAttributes: 45fae22b22a246a0b7f0a8d2157a02bf89fb2e9a - SwipeCellKit: 3972254a826da74609926daf59b08d6c72e619ea - TZImagePickerController: 456f470b5dea97b37226ec7a694994a8663340b2 - UICKeyChainStore: ba3bff2c762b12db1e516f395c837dd25298b05e - YXArtemis_XCFramework: d9a8b9439d7a6c757ed00ada53a6d2dd9b13f9c7 - -PODFILE CHECKSUM: 15749400162d4fb258d1b34877a7808d1c85b00a - -COCOAPODS: 1.16.2 diff --git a/crush/YFLDragCardContainer.swift b/crush/YFLDragCardContainer.swift deleted file mode 100644 index 5306e1b..0000000 --- a/crush/YFLDragCardContainer.swift +++ /dev/null @@ -1,709 +0,0 @@ -// -// YFLDragCardContainer.swift -// Crush -// -// Created by AI Assistant on 2024/12/19. -// Copyright © 2024年 Crush. All rights reserved. -// - -import UIKit -import SnapKit - -// MARK: - 数据源协议 -protocol YFLDragCardContainerDataSource: AnyObject { - /// 数据源个数 - func numberOfRowsInYFLDragCardContainer(_ container: YFLDragCardContainer) -> Int - - /// 显示数据源 - func container(_ container: YFLDragCardContainer, viewForRowsAt index: Int) -> YFLDragCardView -} - -// MARK: - 代理协议 -protocol YFLDragCardContainerDelegate: AnyObject { - /// 点击卡片回调 - func container(_ container: YFLDragCardContainer, didSelectRowAt index: Int) - - /// 拖到最后一张卡片 YES,空,可继续调用reloadData分页数据 - func container(_ container: YFLDragCardContainer, dataSourceIsEmpty isEmpty: Bool) - - /// 当前cardview 是否可以拖拽,默认YES - func container(_ container: YFLDragCardContainer, canDragForCardView cardView: YFLDragCardView) -> Bool - - /// 卡片处于拖拽中回调 - func container(_ container: YFLDragCardContainer, dargingForCardView cardView: YFLDragCardView, direction: ContainerDragDirection, widthRate: Float, heightRate: Float) - - /// 询问这次滑动的方向是否生效,控制 v2.6 ld add - func container(_ container: YFLDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: YFLDragCardView) -> Bool - - /// 卡片拖拽结束回调(卡片消失) - func container(_ container: YFLDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: YFLDragCardView) - - /// 卡片回看 v2.6 添加 - func container(_ container: YFLDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: YFLDragCardView) - - func container(_ container: YFLDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: YFLDragCardView) -} - -// MARK: - 主容器类 -class YFLDragCardContainer: UIView { - - // MARK: - 公开属性 - private(set) var tmpStoreNopeCardView: YFLDragCardView? - private(set) var smallCardMode: Bool = false - private(set) var isMoveIng: Bool = false - - weak var dataSource: YFLDragCardContainerDataSource? - weak var delegate: YFLDragCardContainerDelegate? - - // MARK: - 私有属性 - private var cards: [YFLDragCardView] = [] - private var direction: ContainerDragDirection = .default - private var loadedIndex: Int = 0 - private var firstCardFrame: CGRect = .zero - private var lastCardFrame: CGRect = .zero - private var cardCenter: CGPoint = .zero - private var lastCardTransform: CGAffineTransform = .identity - private var configure: YFLDragConfigure! - - // 🔥 根据上下文推测:动画相关视图 - private var likeOrDislikeShowLogoView: UIView! - private var likeOrDislikeShowImageView: UIImageView! - private var likeLottieView: UIView! // 🔥 推测为Lottie动画视图 - private var dislikeLottieView: UIView! // �� 推测为Lottie动画视图 - private var superlikeLottieView: UIView! // 🔥 推测为Lottie动画视图 - private var superlikeWordsShowBg: UIImageView! - private var superlikeWordsLabel: UILabel! - private var superLikeCountLeft: UILabel! - - // v2.8 - private var currentIntialGestureDirection: ContainerDragDirection = .default - private var canMoveView: Bool = true - - // MARK: - 初始化方法 - convenience init(frame: CGRect) { - self.init(frame: frame, configure: YFLDragCardContainer.setDefaultsConfigures()) - } - - init(frame: CGRect, configure: YFLDragConfigure) { - super.init(frame: frame) - initDataConfigure(configure) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - initDataConfigure(YFLDragCardContainer.setDefaultsConfigures()) - } - - // MARK: - 私有方法 - private static func setDefaultsConfigures() -> YFLDragConfigure { - let configure = YFLDragConfigure() - configure.visableCount = 3 - configure.containerEdge = 16.0 - configure.cardEdge = 0.01 - configure.cardCornerRadius = 8.0 - configure.cardCornerBorderWidth = 0.0 - configure.cardBordColor = UIColor.clear - configure.cardVTopEdage = 0 - configure.cardVBottomEdage = 12 - return configure - } - - private func initDataConfigure(_ configure: YFLDragConfigure) { - resetInitData() - initialLikeDislikesShowViews() - cards = [] - backgroundColor = UIColor.white - self.configure = configure - } - - private func initialLikeDislikesShowViews() { - likeOrDislikeShowLogoView = UIView() - likeOrDislikeShowLogoView.backgroundColor = UIColor.clear - likeOrDislikeShowLogoView.isUserInteractionEnabled = false - likeOrDislikeShowLogoView.alpha = 0 - likeOrDislikeShowLogoView.layer.zPosition = 10 - insertSubview(likeOrDislikeShowLogoView, at: 0) - - likeOrDislikeShowLogoView.snp.makeConstraints { make in - make.center.equalToSuperview() - } - - // 🔥 根据上下文推测:创建Lottie动画视图 - likeLottieView = createLottieView(animationName: "LottieResources/kuolie_swipe_like") - dislikeLottieView = createLottieView(animationName: "LottieResources/kuolie_swipe_dislike") - superlikeLottieView = createLottieView(animationName: "LottieResources/kuolie_superlike") - - // 🔥 根据上下文推测:创建超级喜欢文字背景 - superlikeWordsShowBg = UIImageView() - superlikeWordsShowBg.image = UIImage(named: "kuolie_superlike_words_bg") - superlikeLottieView.addSubview(superlikeWordsShowBg) - superlikeWordsShowBg.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.bottom.equalToSuperview().offset(8) - make.size.equalTo(CGSize(width: 186, height: 32)) - } - - // 🔥 根据上下文推测:创建超级喜欢文字标签 - superlikeWordsLabel = UILabel() - superlikeWordsLabel.textColor = UIColor.white - superlikeWordsLabel.font = UIFont.systemFont(ofSize: 16) // 🔥 推测字体 - superlikeWordsLabel.text = "SUPER LIKE" // 🔥 推测文字 - superlikeLottieView.addSubview(superlikeWordsLabel) - superlikeWordsLabel.snp.makeConstraints { make in - make.center.equalTo(superlikeWordsShowBg) - } - } - - private func createLottieView(animationName: String) -> UIView { - // 🔥 根据上下文推测:创建Lottie动画视图 - let view = UIView() - view.contentMode = .scaleAspectFill - likeOrDislikeShowLogoView.addSubview(view) - view.isHidden = true - view.snp.makeConstraints { make in - make.center.equalTo(likeOrDislikeShowLogoView) - make.size.equalTo(CGSize(width: 120, height: 120)) - } - return view - } - - /// 重置数据(为了二次调用reload) - private func resetInitData() { - loadedIndex = 0 - resetCards() - direction = .default - isMoveIng = false - } - - /// 清掉所有card view - private func resetCards() { - for view in cards { - view.removeFromSuperview() - } - cards.removeAll() - } - - /// 添加子视图 - private func addSubViews() { - guard let dataSource = dataSource else { return } - - let sum = dataSource.numberOfRowsInYFLDragCardContainer(self) - let preLoadViewCount = min(sum, configure.visableCount) - - // 预防越界 - if loadedIndex < sum { - // 当手势滑动,加载第四个,最多创建4个。不存在内存warning。(手势停止,滑动的view没消失,需要干掉多创建的+1) - let targetCount = isMoveIng ? preLoadViewCount + 1 : preLoadViewCount - for i in cards.count..= 3 { - cardView.frame = lastCardFrame - } else { - let frame = cardView.frame - if firstCardFrame.isEmpty { - firstCardFrame = frame - cardCenter = cardView.center - } - } - } - - /// 移动卡片 - private func moveIngStatusChange(_ scale: Float) { - // 如果正在移动,添加第四个 - if !isMoveIng { - isMoveIng = true - addSubViews() - } else { - // 第四个加载完,立马改变没作用在手势上其他cardview的scale - let absScale = min(abs(scale), boundaryRation) - let transFormtxPoor = (secondCardScale - thirdCardScale) / (boundaryRation / absScale) - let frameYPoor: CGFloat = 0 - - for (index, cardView) in cards.enumerated() { - guard index > 0 else { continue } - - switch index { - case 1: - let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + secondCardScale, y: 1) - let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor) - cardView.transform = scaleTransform.concatenating(translateTransform) - case 2: - let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + thirdCardScale, y: 1) - let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor) - cardView.transform = scaleTransform.concatenating(translateTransform) - case 3: - cardView.transform = lastCardTransform - default: - break - } - } - } - } - - // 手势结束 - private func panGesturemMoveFinishOrCancle(_ cardView: YFLDragCardView, direction: ContainerDragDirection, scale: Float, isDisappear: Bool) { - if !isDisappear { // 没有消失,恢复原样 - if currentIntialGestureDirection == .left || currentIntialGestureDirection == .right || currentIntialGestureDirection == .default { - UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.6, options: [.allowUserInteraction, .curveEaseInOut]) { - self.subRestoreCardState() - } completion: { finished in - } - } else { // 其他情况下没有动画 - subRestoreCardState() - } - } else { - // v2.6 - if let delegate = delegate { - if !delegate.container(self, canDragFinishForDirection: direction, forCardView: cardView) { - subRestoreCardState() - return - } - } - - delegate?.container(self, dragDidFinshForDirection: self.direction, forCardView: cardView) - - if direction == .left { - tmpStoreNopeCardView = cardView - } - - let flag = direction == .left ? -1 : 2 - let screenWidth = UIScreen.main.bounds.width - - UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveLinear, .allowUserInteraction]) { - self.isMoveIng = true - cardView.center = CGPoint(x: CGFloat(flag) * screenWidth, y: CGFloat(flag) * screenWidth / CGFloat(scale) + self.cardCenter.y) - } completion: { finished in - self.isMoveIng = false - cardView.removeFromSuperview() - } - - if let index = cards.firstIndex(of: cardView) { - cards.remove(at: index) - } - isMoveIng = false - resetLayoutSubviews() - } - } - - private func subRestoreCardState() { - // 干掉多创建的第四个.重置标量 - if isMoveIng && cards.count > configure.visableCount { - if let lastView = cards.last { - lastView.removeFromSuperview() - cards.removeLast() - loadedIndex = lastView.tag - } - } - isMoveIng = false - resetLayoutSubviews() - } - - // MARK: - 公开方法 - func reloadData() { - guard let dataSource = dataSource else { - assertionFailure("check dataSource and dataSource Methods!") - return - } - - resetInitData() - addSubViews() - resetLayoutSubviews() - } - - func getCurrentShowCardView() -> YFLDragCardView? { - return cards.first - } - - func getCurrentShowCardViewIndex() -> Int { - return cards.first?.tag ?? 0 - } - - /// 手动控制滑动,且不回调代理方法 - func removeCardViewForDirection(_ direction: ContainerDragDirection) { - guard !isMoveIng else { return } - - let screenWidth = UIScreen.main.bounds.width - var cardCenter = CGPoint.zero - var flag = 0 - - guard let currentShowCardView = cards.first else { return } - - switch direction { - case .left: - cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) - flag = -1 - tmpStoreNopeCardView = currentShowCardView - case .right: - cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) - flag = 1 - default: - break - } - - UIView.animate(withDuration: 0.35) { - self.isMoveIng = true - let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) - currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) - currentShowCardView.center = cardCenter - } completion: { finished in - self.isMoveIng = false - currentShowCardView.removeFromSuperview() - if let index = self.cards.firstIndex(of: currentShowCardView) { - self.cards.remove(at: index) - } - self.addSubViews() - self.resetLayoutSubviews() - } - } - - /// app控制滑动,且回调滑动Finish代理 - func removeCardViewWithCallDelegateForDirection(_ direction: ContainerDragDirection) { - guard !isMoveIng else { return } - - let screenWidth = UIScreen.main.bounds.width - var cardCenter = CGPoint.zero - var flag = 0 - - guard let currentShowCardView = cards.first else { return } - - switch direction { - case .left: - cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) - flag = -1 - tmpStoreNopeCardView = currentShowCardView - case .right: - cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) - flag = 1 - default: - break - } - - UIView.animate(withDuration: 0.35) { - let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) - currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) - currentShowCardView.center = cardCenter - } completion: { finished in - currentShowCardView.removeFromSuperview() - if let index = self.cards.firstIndex(of: currentShowCardView) { - self.cards.remove(at: index) - } - self.addSubViews() - self.resetLayoutSubviews() - } - - delegate?.container(self, dragDidFinshForDirection: direction, forCardView: currentShowCardView) - } - - /// 手动回看 Meet 卡片,将之前存的临时CardView 回看回来 - func addCardView(_ cardView: YFLDragCardView?, fromDirection direction: ContainerDragDirection) { - guard !isMoveIng else { return } - - let targetCardView = cardView ?? tmpStoreNopeCardView - guard let cardView = targetCardView else { return } - - let screenWidth = UIScreen.main.bounds.width - var cardCenter = CGPoint.zero - var flag = 0 - - switch direction { - case .left: - cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y) - flag = -1 - tmpStoreNopeCardView = cardView - case .right: - cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y) - flag = 1 - default: - break - } - - cards.insert(cardView, at: 0) - addSubview(cardView) - - cardView.center = cardCenter - let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0) - cardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4) - - cardCenter = CGPoint(x: self.cardCenter.x, y: self.cardCenter.y) - - UIView.animate(withDuration: 0.35) { - cardView.transform = .identity - cardView.center = cardCenter - self.isMoveIng = true - } completion: { finished in - self.isMoveIng = false - self.tmpStoreNopeCardView = nil - } - - delegate?.container(self, lookingBack: direction, forCardView: cardView) - } - - func showNopeLogo(_ show: Bool, widthRate: CGFloat) { - if widthRate == 0 { - if !isLottieAnimationPlaying(dislikeLottieView) { // 🔥 推测方法 - UIView.animate(withDuration: 0.25) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - } - return - } - - if show { - likeLottieView.isHidden = true - superlikeLottieView.isHidden = true - dislikeLottieView.isHidden = false - likeOrDislikeShowLogoView.isHidden = false - likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) - superLikeCountLeft.isHidden = true - } else { - UIView.animate(withDuration: 0.45) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - } - } - - func showLikeLogo(_ show: Bool, widthRate: CGFloat) { - if widthRate == 0 { - if !isLottieAnimationPlaying(likeLottieView) { // 🔥 推测方法 - UIView.animate(withDuration: 0.25) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - } - return - } - - if show { - likeLottieView.isHidden = false - dislikeLottieView.isHidden = true - superlikeLottieView.isHidden = true - likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2) - superLikeCountLeft.isHidden = true - } else { - UIView.animate(withDuration: 0.45) { - self.likeOrDislikeShowLogoView.alpha = 0 - } - } - } - - func showNopeLottie() { - likeLottieView.isHidden = true - superlikeLottieView.isHidden = true - dislikeLottieView.isHidden = false - stopLottieAnimation(dislikeLottieView) // 🔥 推测方法 - likeOrDislikeShowLogoView.alpha = 1 - - playLottieAnimation(dislikeLottieView) { [weak self] finish in - if finish { - self?.likeOrDislikeShowLogoView.alpha = 0 - } - } - } - - func showLikeLottie() { - dislikeLottieView.isHidden = true - superlikeLottieView.isHidden = true - likeLottieView.isHidden = false - stopLottieAnimation(likeLottieView) // 🔥 推测方法 - likeOrDislikeShowLogoView.alpha = 1 - - playLottieAnimation(likeLottieView) { [weak self] finish in - if finish { - self?.likeOrDislikeShowLogoView.alpha = 0 - } - } - } - - func showSuperLikeLottie(complete: @escaping (Bool) -> Void) { - dislikeLottieView.isHidden = true - likeLottieView.isHidden = true - superlikeLottieView.isHidden = false - stopLottieAnimation(superlikeLottieView) // 🔥 推测方法 - likeOrDislikeShowLogoView.alpha = 1 - - playLottieAnimation(superlikeLottieView) { [weak self] finish in - self?.likeOrDislikeShowLogoView.alpha = 0 - complete(finish) - } - } - - func addAdaptForDragCardView(_ cardView: YFLDragCardView) { - let y = configure.containerEdge + configure.cardVTopEdage - let width = frame.size.width - 2 * configure.containerEdge - let height = frame.size.height - 2 * configure.containerEdge - configure.cardVTopEdage - configure.cardVBottomEdage - - cardView.frame = CGRect(x: configure.containerEdge, y: y, width: width, height: height) - cardView.setConfigure(configure) - cardView.YFLDragCardViewLayoutSubviews() - - recordFrame(cardView) - cardView.tag = loadedIndex - - // 添加拖拽手势 - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - cardView.addGestureRecognizer(panGesture) - addSubview(cardView) - sendSubviewToBack(cardView) - } - - func clearTmpStoreNopeCardView() { - tmpStoreNopeCardView?.removeFromSuperview() - tmpStoreNopeCardView = nil - } - - func switchAllCardsToSmallCardMode(_ smallMode: Bool) { - guard smallCardMode != smallMode else { return } - - smallCardMode = smallMode - delegate?.container(self, enterSmallCardMode: smallCardMode, forCardView: getCurrentShowCardView() ?? YFLDragCardView()) - } - - // MARK: - 手势处理 - @objc private func handleTapGesture(_ tap: UITapGestureRecognizer) { - delegate?.container(self, didSelectRowAt: tap.view?.tag ?? 0) - } - - @objc private func handlePanGesture(_ pan: UIPanGestureRecognizer) { - guard let cardView = pan.view as? YFLDragCardView else { return } - - let canEdit = delegate?.container(self, canDragForCardView: cardView) ?? true - - if canEdit { - switch pan.state { - case .began: - let point = pan.translation(in: self) - doVDirectionLogicByPoint(point, forCardView: cardView) - - case .changed: - guard currentIntialGestureDirection != .up && currentIntialGestureDirection != .down else { return } - - if let delegate = delegate { - // 计算横向滑动比例 >0 向右 <0 向左 - let horizionSliderRate = Float((pan.view?.center.x ?? 0) - cardCenter.x) / Float(cardCenter.x) - let verticalSliderRate = Float((pan.view?.center.y ?? 0) - cardCenter.y) / Float(cardCenter.y) - - // 正在滑动,需要创建第四个。 - moveIngStatusChange(horizionSliderRate) - - if currentIntialGestureDirection == .default { - // 再次决定方向 - let point = pan.translation(in: self) - doVDirectionLogicByPoint(point, forCardView: cardView) - } else { // 已经确定了方向,且走到这儿,肯定是左右滑 - // 以自身的左上角为原点;每次移动后,原点都置0;计算的是相对于上一个位置的偏移; - let point = pan.translation(in: self) - cardView.center = CGPoint(x: (pan.view?.center.x ?? 0) + point.x * 0.5, y: pan.view?.center.y ?? 0) - - // 当angle为正值时,逆时针旋转坐标系统,反之顺时针旋转坐标系统 - let rotationAngle = ((pan.view?.center.x ?? 0) - cardCenter.x) / cardCenter.x * (.pi / 4 / 12) - cardView.transform = cardView.originTransForm.rotated(by: rotationAngle) - pan.setTranslation(.zero, in: self) // 设置坐标原点位上次的坐标 - } - - if horizionSliderRate > 0 { - direction = .right - } else if horizionSliderRate < 0 { - direction = .left - } else { - direction = .default - } - - if currentIntialGestureDirection == .default { - currentIntialGestureDirection = direction - } - - delegate.container(self, dargingForCardView: cardView, direction: direction, widthRate: horizionSliderRate, heightRate: verticalSliderRate) - } - - case .ended, .cancelled: - // 还原card位置,或者消失card - let horizionSliderRate = Float((pan.view?.center.x ?? 0) - cardCenter.x) / Float(cardCenter.x) - let moveY = (pan.view?.center.y ?? 0) - cardCenter.y - let moveX = (pan.view?.center.x ?? 0) - cardCenter.x - - panGesturemMoveFinishOrCancle(cardView, direction: direction, scale: Float(moveX / moveY), isDisappear: abs(horizionSliderRate) > boundaryRation) - - for per in cards { // 上下滑完后,不隐藏后面的cardView - per.isHidden = false - } - - currentIntialGestureDirection = .default - - default: - break - } - } - } - - // MARK: - 辅助方法 - private func doVDirectionLogicByPoint(_ point: CGPoint, forCardView cardView: YFLDragCardView) { - // option2: 不单独处理竖直方向手势 - if point.x == 0 { - currentIntialGestureDirection = .default - } else if point.x < 0 { - currentIntialGestureDirection = .left - } else { - currentIntialGestureDirection = .right - } - } - - // 🔥 根据上下文推测的Lottie动画相关方法 - private func isLottieAnimationPlaying(_ view: UIView) -> Bool { - // 🔥 推测:检查Lottie动画是否正在播放 - return false - } - - private func stopLottieAnimation(_ view: UIView) { - // 🔥 推测:停止Lottie动画 - } - - private func playLottieAnimation(_ view: UIView, completion: @escaping (Bool) -> Void) { - // 🔥 推测:播放Lottie动画 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - completion(true) - } - } -}