Procházet zdrojové kódy

更新一批功能

100Years před 2 měsíci
rodič
revize
f35d5355b8
29 změnil soubory, kde provedl 1767 přidání a 285 odebrání
  1. 4 4
      TSSmalCoacopods/Classes/BaseClass/TSBaseCollectionCell.swift
  2. 35 50
      TSSmalCoacopods/Classes/BaseClass/TSBaseVC.swift
  3. 1 1
      TSSmalCoacopods/Classes/BaseClass/TSBaseView.swift
  4. 5 5
      TSSmalCoacopods/Classes/Config/TSConfig.swift
  5. 59 6
      TSSmalCoacopods/Classes/Ex/Int+Ex.swift
  6. 27 1
      TSSmalCoacopods/Classes/Ex/NSString+Ex.swift
  7. 13 0
      TSSmalCoacopods/Classes/Ex/SwiftUI/Text+Ex.swift
  8. 38 1
      TSSmalCoacopods/Classes/Ex/UIApplication+Ex.swift
  9. 20 0
      TSSmalCoacopods/Classes/Ex/UIButton+Ex.swift
  10. 1 1
      TSSmalCoacopods/Classes/Ex/UIFont+Ex.swift
  11. 128 0
      TSSmalCoacopods/Classes/Ex/UIImage+Ex.swift
  12. 69 5
      TSSmalCoacopods/Classes/Ex/UIImageView+Ex.swift
  13. 125 2
      TSSmalCoacopods/Classes/Ex/UIView+Ex.swift
  14. 6 4
      TSSmalCoacopods/Classes/Ex/UIViewController+Ex.swift
  15. 20 1
      TSSmalCoacopods/Classes/GlobalImports/GlobalImports.swift
  16. 151 0
      TSSmalCoacopods/Classes/Tool/TSBusinessFileManager.swift
  17. 25 3
      TSSmalCoacopods/Classes/Tool/TSCommonTool/TSCommonTool+Down.swift
  18. 82 82
      TSSmalCoacopods/Classes/Tool/TSCommonTool/TSCommonTool+MultDown.swift
  19. 74 74
      TSSmalCoacopods/Classes/Tool/TSCommonTool/TSMultiTaskDownloader.swift
  20. 76 0
      TSSmalCoacopods/Classes/Tool/TSCustomAlertController.swift
  21. 111 0
      TSSmalCoacopods/Classes/Tool/TSDownloadManager.swift
  22. 50 34
      TSSmalCoacopods/Classes/Tool/TSFileManagerTool.swift
  23. 190 0
      TSSmalCoacopods/Classes/Tool/TSImageCompress.swift
  24. 52 8
      TSSmalCoacopods/Classes/View/TSPlaceholderTextView/TSPlaceholderTextView.swift
  25. 68 0
      TSSmalCoacopods/Classes/View/TSProgressSlider/TSProgressSlider.swift
  26. 186 0
      TSSmalCoacopods/Classes/View/TSReusableCollectionView/TSSimpleCollectionView.swift
  27. 120 0
      TSSmalCoacopods/Classes/View/TSSaveSuccessTool/TSSaveSuccessTool.swift
  28. 5 1
      TSSmalCoacopods/Classes/View/UICollectionView+Component/CollectionViewComponent.swift
  29. 26 2
      TSSmalCoacopods/Classes/View/UIStackView/TSCustomStackView.swift

+ 4 - 4
TSSmalCoacopods/Classes/BaseClass/TSBaseCollectionCell.swift

@@ -49,7 +49,7 @@ open class TSBaseCollectionCell: UICollectionViewCell,TSComponentView  {
         debugPrint("<---deinit \(String(describing: self))")
     }
     
-    public var indexPath:IndexPath?{
+    public var cellIndexPath:IndexPath?{
         if let attributes = colAttributes , let IndexPath = attributes[kIndexPath] as? IndexPath{
             return IndexPath
         }
@@ -64,7 +64,7 @@ open class TSBaseCollectionCell: UICollectionViewCell,TSComponentView  {
     }
     
     public func actionHandler(any:Any?){
-        if let sectionActionHandler = itemActionHandler ,let indexPath = indexPath{
+        if let sectionActionHandler = itemActionHandler ,let indexPath = cellIndexPath{
             sectionActionHandler(any,indexPath)
         }
     }
@@ -121,7 +121,7 @@ open class TSBaseCollectionnReusableView : UICollectionReusableView ,TSComponent
 }
 
 extension TSBaseCollectionnReusableView{
-    public var indexPath:IndexPath?{
+    public var cellIndexPath:IndexPath?{
         if let attributes = colAttributes , let IndexPath = attributes[kIndexPath] as? IndexPath{
             return IndexPath
         }
@@ -136,7 +136,7 @@ extension TSBaseCollectionnReusableView{
     }
     
     public func actionHandler(any:Any?){
-        if let sectionActionHandler = sectionActionHandler ,let indexPath = indexPath{
+        if let sectionActionHandler = sectionActionHandler ,let indexPath = cellIndexPath{
             sectionActionHandler(any,indexPath)
         }
     }

+ 35 - 50
TSSmalCoacopods/Classes/BaseClass/TSBaseVC.swift

@@ -9,6 +9,8 @@ import UIKit
 import Combine
 
 open class TSBaseVC: UIViewController {
+  
+    public var closePageComplete:(()->Void)?
     
     public var cancellable: [AnyCancellable] = []
     
@@ -41,13 +43,7 @@ open class TSBaseVC: UIViewController {
         view.backgroundColor = .clear
         return view
     }()
-    
-//    public lazy var nullView: UIView = {
-//        let view = UIView()
-//        view.backgroundColor = .clear
-//        return view
-//    }()
-    
+
     public lazy var netWorkView: UIView = {
         let view = UIView()
         view.backgroundColor = .clear
@@ -132,57 +128,16 @@ open class TSBaseVC: UIViewController {
             make.edges.equalToSuperview()
         }
     }
-    
-//    func useTransparentBlurredNavigationBar() {
-//        navBarView.removeFromSuperview()
-//        edgesForExtendedLayout = .all
-//        
-//        view.addSubview(navBlurView)
-//        navBlurView.snp.makeConstraints { make in
-//            make.left.top.right.equalToSuperview()
-//            make.height.equalTo(k_Nav_Height)
-//        }
-//        
-//        setClearNavigationBar()
-//        navBarView.titleNavBtn?.titleLabel?.alpha = 0.0
-//        navBlurView.contentView.addSubview(navBarView)
-//        
-//        navBarView.snp.makeConstraints { make in
-//            make.edges.equalToSuperview()
-//        }
-//        
-//        contentView.snp.updateConstraints { make in
-//            make.top.equalToSuperview()
-//        }
-//    }
-//    
-//    func handleTransparentBlurredNavigationBar(scrollViewY: CGFloat) {
-//        if scrollViewY >= - kStatusBar_Height {
-//            navBlurView.effect = blurEffect
-//            navBarView.titleNavBtn?.titleLabel?.alpha = 1.0
-//        } else {
-//            navBlurView.effect = nil
-//            navBarView.titleNavBtn?.titleLabel?.alpha = 0.0
-//        }
-//    }
-    
-    
 
-    
-    // ... 其他方法根据需要重写 ...
-    
-    
-    
-    
-    
+
     open override func viewWillAppear(_ animated: Bool) {
-//        super.viewWillAppear(animated)
         debugPrint("进入------>\(String(describing: type(of: self)))")
     }
 
     deinit {
         NotificationCenter.default.removeObserver(self)
         cancellable.removeAll()
+        closePageComplete?()
         debugPrint("♻️♻️♻️ TGRootViewController -> \(type(of: self)) deinit ♻️♻️♻️")
     }
 
@@ -236,4 +191,34 @@ extension TSBaseVC {
             make.top.equalTo(isHidden ? 0 : k_Nav_Height)
         }
     }
+    
+    public var isViewVisible: Bool {
+        return isViewLoaded && view.window != nil && presentedViewController == nil
+    }
+    
+    public func addPullDownClosePage() {
+        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleDismissPan(_:)))
+        view.addGestureRecognizer(panGesture)
+    }
+
+    // 手势处理方法
+     @objc public func handleDismissPan(_ gesture: UIPanGestureRecognizer) {
+        let translation = gesture.translation(in: view)
+        let progress = translation.y / view.bounds.height
+        
+        switch gesture.state {
+        case .changed:
+            view.transform = CGAffineTransform(translationX: 0, y: max(0, translation.y))
+        case .ended:
+            if progress > 0.5 || gesture.velocity(in: view).y > 1000 {
+                dismiss(animated: true)
+            } else {
+                UIView.animate(withDuration: 0.3) {
+                    self.view.transform = .identity
+                }
+            }
+        default:
+            break
+        }
+    }
 }

+ 1 - 1
TSSmalCoacopods/Classes/BaseClass/TSBaseView.swift

@@ -6,7 +6,7 @@
 //
 
 open class TSBaseView : UIView {
-    
+
     public lazy var contentView:UIView = {
         let view = UIView()
         view.backgroundColor = .clear

+ 5 - 5
TSSmalCoacopods/Classes/Config/TSConfig.swift

@@ -30,7 +30,7 @@ open class TSConfig: NSObject {
         return nil
     }()
     
-    static let isLanguageZh: Bool = {
+    public static let isLanguageZh: Bool = {
         if appLanguage.lowercased().hasPrefix("zh") {
             return true
         }
@@ -51,10 +51,10 @@ open class TSConfig: NSObject {
         return ""
     }
     
-    static var isChinaRegion: Bool {
-#if DEBUG
-        return true
-#endif
+    public static var isChinaRegion: Bool {
+//#if DEBUG
+//        return true
+//#endif
         let localeId = Locale.current.identifier
         return localeId.contains("_CN")
     }

+ 59 - 6
TSSmalCoacopods/Classes/Ex/Int+Ex.swift

@@ -6,11 +6,11 @@
 //
 
 public extension Int {
-    var localNumber: String? {
+    public var localNumber: String? {
         return localNumber()
     }
     
-    func localNumber(_ minimumIntegerDigits: Int? = nil, locale: Locale? = nil) -> String? {
+    public func localNumber(_ minimumIntegerDigits: Int? = nil, locale: Locale? = nil) -> String? {
         let formatter = NumberFormatter()
         if let locale = locale {
             formatter.locale = locale
@@ -27,7 +27,7 @@ public extension Int {
     }
 }
 public extension Locale {
-    static var local: Locale {
+    public static var local: Locale {
         return Locale(identifier: TSConfig.appLanguage)
     }
 }
@@ -35,7 +35,7 @@ public extension Locale {
 public extension Int {
     /// 将秒数转换为 "HH:mm" 格式的字符串
     /// - Returns: 转换后的字符串
-    func toTimeString() -> String {
+    public func toTimeString() -> String {
         // 计算小时和分钟
         let hours = self / 3600
         let minutes = (self % 3600) / 60
@@ -46,7 +46,7 @@ public extension Int {
     
     /// 将秒数转换为 "HH:mm:ss" 格式的字符串
     /// - Returns: 转换后的字符串
-    func toHourMinuteSecondString() -> String {
+    public func toHourMinuteSecondString() -> String {
         // 计算小时、分钟和秒
         let hours = self / 3600
         let minutes = (self % 3600) / 60
@@ -56,7 +56,7 @@ public extension Int {
         return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
     }
     
-    static func timestampInt() -> Int {
+    public static func timestampInt() -> Int {
          let currentTimestamp = Int(Date().timeIntervalSince1970)
          return currentTimestamp
      }
@@ -69,3 +69,56 @@ public extension Int {
         return Int(OSAtomicIncrement64(&uniqueCounter))
     }
 }
+
+public extension TimeInterval {
+    public var mmss: String {
+        let minute = Int(self) / 60
+        let second = Int(self) % 60
+        return String(format: "%02d", minute) + ":" + String(format: "%02d", second)
+    }
+
+    public var mmsshh: String {
+        let hour = Int(self) / 3600
+        let minute = (Int(self) % 3600) / 60
+        let second = Int(self) % 60
+
+        if hour > 0 {
+            return String(format: "%02d:%02d:%02d", hour, minute, second)
+        } else {
+            return mmss
+        }
+    }
+
+    public var hhmmss: String {
+        let hour = Int(self) / 3600
+        let minute = (Int(self) % 3600) / 60
+        let second = Int(self) % 60
+        return String(format: "%02d:%02d:%02d", hour, minute, second)
+    }
+}
+public extension Int {
+    public var mmss: String {
+        let minute = Int(self) / 60
+        let second = Int(self) % 60
+        return String(format: "%02d", minute) + ":" + String(format: "%02d", second)
+    }
+
+    public var mmsshh: String {
+        let hour = Int(self) / 3600
+        let minute = (Int(self) % 3600) / 60
+        let second = Int(self) % 60
+
+        if hour > 0 {
+            return String(format: "%02d:%02d:%02d", hour, minute, second)
+        } else {
+            return mmss
+        }
+    }
+
+    public var hhmmss: String {
+        let hour = Int(self) / 3600
+        let minute = (Int(self) % 3600) / 60
+        let second = Int(self) % 60
+        return String(format: "%02d:%02d:%02d", hour, minute, second)
+    }
+}

+ 27 - 1
TSSmalCoacopods/Classes/Ex/NSString+Ex.swift

@@ -108,8 +108,12 @@ public extension String {
 }
 
 public extension String {
-    
 
+    var localized:String {
+        return NSLocalizedString(self, comment: self)
+//        return self
+    }
+        
 }
 
 
@@ -345,3 +349,25 @@ public extension String {
     
     
 }
+
+public extension String {
+    func removeTimestampFromEnd() -> String {
+        let string = self
+//        // 正则表达式:匹配末尾的10位或13位数字
+//        let pattern = "\\d{10}$|\\d{13}$"
+//        guard let regex = try? NSRegularExpression(pattern: pattern) else {
+//            return string
+//        }
+//        let range = NSRange(location: 0, length: string.utf16.count)
+//        return regex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
+        
+        // 正则表达式:匹配(可选下划线)后跟10位或13位数字结尾
+        let pattern = "_?\\d{10}$|_?\\d{13}$"
+        guard let regex = try? NSRegularExpression(pattern: pattern) else {
+            return string
+        }
+        let range = NSRange(location: 0, length: string.utf16.count)
+        return regex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
+        
+    }
+}

+ 13 - 0
TSSmalCoacopods/Classes/Ex/SwiftUI/Text+Ex.swift

@@ -19,3 +19,16 @@ public extension Text {
             )
     }
 }
+public extension View {
+    func gradientForeground(colors: [Color], startPoint: UnitPoint, endPoint: UnitPoint) -> some View {
+        self
+            .overlay(
+                LinearGradient(
+                    gradient: Gradient(colors: colors),
+                    startPoint: startPoint,
+                    endPoint: endPoint
+                )
+                .mask(self)
+            )
+    }
+}

+ 38 - 1
TSSmalCoacopods/Classes/Ex/UIApplication+Ex.swift

@@ -6,7 +6,44 @@
 //
 
 import UIKit
-
+import AVFoundation
 public extension UIApplication {
+    // 获取系统音量
+    public static func getSystemVolume() -> Float {
+        let audioSession = AVAudioSession.sharedInstance()
+        do {
+            try audioSession.setCategory(.playback) // 设置音频会话为播放模式
+            return audioSession.outputVolume
+        } catch {
+            print("Failed to set audio session category.")
+            return -1.0
+        }
+    }
+    
+
+    public enum TSVersionCompareResult {
+        case newer
+        case older
+        case same
+    }
 
+    public static func compareAppVersions(newVersion1: String, oldVersion: String) -> TSVersionCompareResult {
+        let components1 = newVersion1.components(separatedBy: ".")
+        let components2 = oldVersion.components(separatedBy: ".")
+        
+        let maxLength = max(components1.count, components2.count)
+        
+        for i in 0..<maxLength {
+            let num1 = i < components1.count ? Int(components1[i]) ?? 0 : 0
+            let num2 = i < components2.count ? Int(components2[i]) ?? 0 : 0
+            
+            if num1 > num2 {
+                return .newer
+            } else if num1 < num2 {
+                return .older
+            }
+        }
+        
+        return .same
+    }
 }

+ 20 - 0
TSSmalCoacopods/Classes/Ex/UIButton+Ex.swift

@@ -134,3 +134,23 @@ public class TSUIExpandedTouchButton: UIButton {
         return expandedBounds.contains(point)
     }
 }
+
+
+public class TSVerticalButton: UIButton {
+    var spacing: CGFloat = 4 {
+        didSet { setNeedsLayout() }
+    }
+
+    public override func layoutSubviews() {
+        super.layoutSubviews()
+        guard let imageView = imageView, let titleLabel = titleLabel else { return }
+
+        // 图片居中靠上
+        imageView.frame.origin.y = (bounds.height - imageView.frame.height - titleLabel.frame.height - spacing) / 2
+        imageView.center.x = bounds.width / 2
+
+        // 文字居中靠下
+        titleLabel.frame.origin.y = imageView.frame.maxY + spacing
+        titleLabel.center.x = bounds.width / 2
+    }
+}

+ 1 - 1
TSSmalCoacopods/Classes/Ex/UIFont+Ex.swift

@@ -8,7 +8,7 @@
 public typealias FontName = String
 
 public extension FontName {
-    static public let PingFangSC           = "PingFangSC"
+    static public let PingFangSC   = "PingFangSC"
 }
 
 

+ 128 - 0
TSSmalCoacopods/Classes/Ex/UIImage+Ex.swift

@@ -131,6 +131,33 @@ public extension UIImage {
     public func resizableImage(capInsets:UIEdgeInsets) -> UIImage{
         return resizableImage(withCapInsets: capInsets, resizingMode: .stretch)
     }
+    
+    
+  
+        // 创建纯色图片
+    convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
+        let rect = CGRect(origin: .zero, size: size)
+        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
+        color.setFill()
+        UIRectFill(rect)
+        let image = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        
+        guard let cgImage = image?.cgImage else { return nil }
+        self.init(cgImage: cgImage)
+    }
+        
+        // 创建圆形图片
+    static func circle(diameter: CGFloat, color: UIColor) -> UIImage {
+        let size = CGSize(width: diameter, height: diameter)
+        let renderer = UIGraphicsImageRenderer(size: size)
+        return renderer.image { context in
+            let rect = CGRect(origin: .zero, size: size)
+            color.setFill()
+            UIBezierPath(ovalIn: rect).fill()
+        }
+    }
+
 }
 
 public extension UIImage {
@@ -174,5 +201,106 @@ public extension UIImage {
         }
         return nil
     }
+    
+    /// 判断图片是否大于指定大小(默认10MB) 
+   func isLargerThan(byteSize: Int) -> Bool {
+
+//       // 优先尝试JPEG压缩(更接近实际文件大小)
+//       if let jpegData = self.jpegData(compressionQuality: 1.0) {
+//           print("JPEG格式大小: \(Double(jpegData.count) / (1024 * 1024)) MB")
+//           return jpegData.count > byteSize
+//       }
+       
+       // 透明图片回退到PNG
+       if let pngData = self.pngData() {
+           print("PNG格式大小: \(Double(pngData.count) / (1024 * 1024)) MB")
+           return pngData.count > byteSize
+       }
+       
+       return false // 无法获取数据时默认不限制
+   }
 }
 
+//旋转图片
+public extension UIImage {
+
+    /// 按角度旋转图片
+    /// - Parameter degrees: 旋转正数为顺时针,负数为逆时针)
+    /// - Returns: 旋转后的新图片
+    func rotated(byDegrees degrees: CGFloat) -> UIImage? {
+        let radians = degrees * .pi / 180
+        return rotated(byRadians: radians)
+    }
+    
+    /// 按弧度旋转图片
+    /// - Parameter radians: 旋转弧度
+    /// - Returns: 旋转后的新图片
+    func rotated(byRadians radians: CGFloat) -> UIImage? {
+        // 计算旋转后的新边界大小
+        let rotatedSize = CGRect(origin: .zero, size: size)
+            .applying(CGAffineTransform(rotationAngle: radians))
+            .integral.size
+        
+        // 开始图形上下文
+        UIGraphicsBeginImageContextWithOptions(rotatedSize, false, scale)
+        guard let context = UIGraphicsGetCurrentContext() else { return nil }
+        
+        // 移动和旋转坐标系
+        context.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2)
+        context.rotate(by: radians)
+        
+        // 绘制图像
+        draw(in: CGRect(
+            x: -size.width / 2,
+            y: -size.height / 2,
+            width: size.width,
+            height: size.height
+        ))
+        
+        // 获取旋转后的图像
+        let rotatedImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        
+        return rotatedImage
+    }
+    
+    /// 固定角度旋转(90度、180度、270度)
+    /// - Parameter rotation: 旋转角度
+    /// - Returns: 旋转后的新图片
+    func rotated(by rotation: Rotation) -> UIImage? {
+        switch rotation {
+        case .degrees90:
+            return rotated(byDegrees: 90)
+        case .degrees180:
+            return rotated(byDegrees: 180)
+        case .degrees270:
+            return rotated(byDegrees: 270)
+        }
+    }
+    
+    // MARK: - 辅助类型
+    
+    enum Rotation {
+        case degrees90
+        case degrees180
+        case degrees270
+    }
+    
+    /// 保存图片到文件系统
+    /// - Parameters:
+    ///   - url: 目标URL
+    ///   - compressionQuality: JPEG压缩质量(0.0-1.0)
+    /// - Throws: 可能抛出错误
+    func saveToFile(at url: URL, compressionQuality: CGFloat = 1.0) {
+        guard let data = self.jpegData(compressionQuality: compressionQuality) else {
+            debugPrint("无法转换为JPEG数据")
+            return
+        }
+        
+        do {
+            try data.write(to: url)
+        } catch {
+            debugPrint(error)
+        }
+    }
+}

+ 69 - 5
TSSmalCoacopods/Classes/Ex/UIImageView+Ex.swift

@@ -35,13 +35,13 @@ public extension UIImageView {
     ///   - contentMode: 内容模式,默认为 `.scaleAspectFit`
     ///   - backgroundColor: 背景颜色,默认为透明
     /// - Returns: 配置完成的 UIImageView 实例
-    static public func createImageView(imageName: String,
+    static public func createImageView(imageName: String? = nil,
                                 contentMode: UIView.ContentMode = .scaleAspectFit,
                                 backgroundColor: UIColor = .clear,
                                 corner: CGFloat = 0.0) -> UIImageView {
         let imageView = UIImageView()
 
-        if imageName.count > 0 {
+        if let imageName = imageName ,imageName.count > 0 {
             imageView.image = UIImage(named: imageName)
         }
         
@@ -62,15 +62,19 @@ public extension UIImageView {
     static public func createAsyncImageView(urlString: String?,
                                      placeholder:UIImage?,
                                      contentMode: UIView.ContentMode = .scaleAspectFit,
+                                        adaptiveMode:Bool = false,
                                      backgroundColor: UIColor = .clear,
                                      showLoading: Bool = false,
+                                    progressBlock: ((Float)->Void)? = nil,
                                      completion: ((UIImage?) -> Void)? = nil) -> UIImageView {
         let imageView = UIImageView()
         imageView.setAsyncImage(urlString: urlString,
                                 placeholder:placeholder,
                                 contentMode:contentMode,
+                                adaptiveMode:adaptiveMode,
                                 backgroundColor:backgroundColor,
                                 showLoading:showLoading,
+                                progressBlock:progressBlock,
                                 completion:completion)
         return imageView
     }
@@ -80,8 +84,10 @@ public extension UIImageView {
     public func setAsyncImage(urlString: String?,
                      placeholder: UIImage? = nil,
                      contentMode: UIView.ContentMode = .scaleAspectFit,
+                 adaptiveMode:Bool = false,
                  backgroundColor: UIColor = .clear,
                       showLoading: Bool = false,
+                        progressBlock: ((Float)->Void)? = nil,
                       completion: ((UIImage?) -> Void)? = nil){
         let imageView = self
         imageView.contentMode = contentMode
@@ -107,7 +113,10 @@ public extension UIImageView {
             imageView.kf.setImage(with: url,
                  placeholder: placeholder,
                      options: nil,
-               progressBlock: nil) { result in
+                progressBlock: { receivedSize, totalSize in
+                let progress = receivedSize/totalSize
+                progressBlock?(Float(progress))
+            }){ result in
                 
                 if let image = try? result.get().image {
                     kDelayMainShort {
@@ -120,25 +129,41 @@ public extension UIImageView {
             
         }else if urlString.contains("/") {
             imageView.image = UIImage(contentsOfFile: urlString.fillCachePath)
+            completion?(imageView.image)
         }else {
             if let image = UIImage(named: urlString) {
                 imageView.image = image
+                completion?(image)
             }
         }
     }
 
 }
 
-extension UIImageView {
+public extension UIImageView {
     
     static public func createRightArrow() -> UIImageView {
         let imageView = UIImageView()
         imageView.image = UIImage(named: "white_right_arrow")
         return imageView
     }
+    
+    /// 根据图片比例自动选择最佳缩放模式
+    public func adaptiveScale() {
+//        guard let image = image else { return }
+//        
+//        let viewSize = bounds.size
+//        let imageRatio = image.size.width / image.size.height
+//        let viewRatio = viewSize.width / viewSize.height
+//        
+//        let contentNewMode:UIView.ContentMode = imageRatio > viewRatio ? .scaleAspectFill : .scaleAspectFit
+//        self.contentMode = contentNewMode
+//        
+//        dePrint("UIImageView.adaptiveScale contentMode =\(contentNewMode)")
+    }
 }
 
-extension UIImageView {
+public extension UIImageView {
     public func setImage(_ image: UIImage?, duration: CFTimeInterval = 0.2, animated: Bool = true) {
         if let image = image {
             if animated {
@@ -155,3 +180,42 @@ extension UIImageView {
         }
     }
 }
+
+
+public extension UIImageView {
+
+    static func downloadImageWithProgress(
+        urlString: String,
+        progressHandler: ((Float) -> Void)? = nil,
+        completion: @escaping (UIImage?) -> Void
+    ) {
+
+        guard let url = URL(string: urlString) else {
+            completion(nil)
+            return
+        }
+        
+        KingfisherManager.shared.retrieveImage(
+            with: url,
+            options: [],
+            progressBlock: { receivedSize, totalSize in
+                let progress = Float(receivedSize) / Float(totalSize)
+                DispatchQueue.main.async {
+                    progressHandler?(progress) // 回传进度(主线程)
+                }
+            },
+            completionHandler: { result in
+                DispatchQueue.main.async {
+                    switch result {
+                    case .success(let value):
+                        completion(value.image)
+                    case .failure:
+                        completion(nil)
+                    }
+                }
+            }
+        )
+    }
+    
+    
+}

+ 125 - 2
TSSmalCoacopods/Classes/Ex/UIView+Ex.swift

@@ -95,7 +95,7 @@ public extension UIView {
 
 public extension UIView {
 
-    enum MaskContentMode {
+    public enum MaskContentMode {
             case scaleToFill
             case scaleAspectFit
             case scaleAspectFill
@@ -106,7 +106,7 @@ public extension UIView {
         ///   - image: 用作蒙版的图片
         ///   - contentMode: 蒙版的内容模式
         ///   - scaleFactor: 缩小比例,默认 1.0 表示不缩放,0.8 表示缩小为 80%
-        func setImageMask(image: UIImage, contentMode: MaskContentMode = .scaleAspectFit, scaleFactor: CGFloat = 1.0) {
+    public func setImageMask(image: UIImage, contentMode: MaskContentMode = .scaleAspectFit, scaleFactor: CGFloat = 1.0) {
             guard scaleFactor > 0 && scaleFactor <= 1 else {
                 print("Invalid scaleFactor. It must be in the range (0, 1].")
                 return
@@ -289,6 +289,14 @@ public extension UIView {
     
 }
 
+public extension UIView {
+    static func creatColor(color:UIColor) -> UIView {
+        let view = UIView()
+        view.backgroundColor = color
+        return view
+    }
+}
+
 
 public extension UIView {
     func removeAllSubViews() {
@@ -297,3 +305,118 @@ public extension UIView {
         }
     }
 }
+
+
+public func kGetSubFrame(superSize: CGSize, subViewSize: CGSize, contentMode: UIView.ContentMode = .scaleAspectFit, scaleFactor: CGFloat = 1.0)-> CGRect {
+    guard scaleFactor > 0 && scaleFactor <= 1 else {
+        print("Invalid scaleFactor. It must be in the range (0, 1].")
+        return CGRect(origin: .zero, size: superSize)
+    }
+    
+    var maskFrame: CGRect = .zero
+    switch contentMode {
+    case .scaleToFill:
+        // 拉伸填充整个视图
+        maskFrame = CGRect(origin: .zero, size: superSize)
+        
+    case .scaleAspectFit:
+        // 按比例缩放,适配视图并居中
+        let aspectWidth = superSize.width / subViewSize.width
+        let aspectHeight = superSize.height / subViewSize.height
+        let aspectRatio = min(aspectWidth, aspectHeight) // 按比例适配
+        
+        let scaledWidth = subViewSize.width * aspectRatio * scaleFactor
+        let scaledHeight = subViewSize.height * aspectRatio * scaleFactor
+        let xOffset = (superSize.width - scaledWidth) / 2
+        let yOffset = (superSize.height - scaledHeight) / 2
+        
+        maskFrame = CGRect(x: xOffset, y: yOffset, width: scaledWidth, height: scaledHeight)
+        
+    case .scaleAspectFill:
+        // 按比例缩放,填充视图并裁剪多余部分
+        let aspectWidth = superSize.width / subViewSize.width
+        let aspectHeight = superSize.height / subViewSize.height
+        let aspectRatio = max(aspectWidth, aspectHeight) // 按比例填充
+        
+        let scaledWidth = subViewSize.width * aspectRatio * scaleFactor
+        let scaledHeight = subViewSize.height * aspectRatio * scaleFactor
+        let xOffset = (superSize.width - scaledWidth) / 2
+        let yOffset = (superSize.height - scaledHeight) / 2
+        
+        maskFrame = CGRect(x: xOffset, y: yOffset, width: scaledWidth, height: scaledHeight)
+    default:
+        maskFrame = CGRect(origin: .zero, size: superSize)
+    }
+    
+    return maskFrame
+}
+
+
+public extension UIView {
+    /// 添加渐变色边框
+    /// - Parameters:
+    ///   - colors: 渐变色数组
+    ///   - width: 边框宽度
+    ///   - radius: 圆角半径(nil 时使用视图的现有圆角)
+    ///   - startPoint: 渐变起点 (默认从左到右)
+    ///   - endPoint: 渐变终点 (默认从左到右)
+    func addGradientBorder(
+        colors: [UIColor],
+        width: CGFloat = 3,
+        radius: CGFloat? = nil,
+        startPoint: CGPoint = CGPoint(x: 0, y: 0.5),
+        endPoint: CGPoint = CGPoint(x: 1, y: 0.5)
+    ) {
+        // 移除旧的渐变边框
+        removeGradientBorder()
+        
+        let gradientLayer = CAGradientLayer()
+        gradientLayer.name = "GradientBorder"
+        gradientLayer.frame = bounds
+        gradientLayer.colors = colors.map { $0.cgColor }
+        gradientLayer.startPoint = startPoint
+        gradientLayer.endPoint = endPoint
+        
+        let cornerRadius = radius ?? layer.cornerRadius
+        let path = UIBezierPath(
+            roundedRect: bounds.insetBy(dx: width/2, dy: width/2),
+            cornerRadius: cornerRadius
+        )
+        
+        let shapeLayer = CAShapeLayer()
+        shapeLayer.name = "GradientBorderMask"
+        shapeLayer.path = path.cgPath
+        shapeLayer.lineWidth = width
+        shapeLayer.fillColor = nil
+        shapeLayer.strokeColor = UIColor.black.cgColor
+        
+        gradientLayer.mask = shapeLayer
+        layer.addSublayer(gradientLayer)
+    }
+    
+    /// 更新渐变色边框的frame(在layoutSubviews中调用)
+    func updateGradientBorder() {
+        guard let gradientLayer = layer.sublayers?.first(where: { $0.name == "GradientBorder" }) as? CAGradientLayer,
+              let shapeLayer = gradientLayer.mask as? CAShapeLayer else {
+            return
+        }
+        
+        gradientLayer.frame = bounds
+        
+        let width = shapeLayer.lineWidth
+        let cornerRadius = layer.cornerRadius
+        let path = UIBezierPath(
+            roundedRect: bounds.insetBy(dx: width/2, dy: width/2),
+            cornerRadius: cornerRadius
+        )
+        
+        shapeLayer.path = path.cgPath
+    }
+    
+    /// 移除渐变色边框
+    func removeGradientBorder() {
+        layer.sublayers?.filter { $0.name == "GradientBorder" }.forEach {
+            $0.removeFromSuperlayer()
+        }
+    }
+}

+ 6 - 4
TSSmalCoacopods/Classes/Ex/UIViewController+Ex.swift

@@ -35,12 +35,12 @@ public extension UIViewController {
         }
         
         // 添加按钮
-        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
+        let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel) { _ in
             cancelHandler?()
         }
         alert.addAction(cancelAction)
         
-        let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in
+        let deleteAction = UIAlertAction(title: "Delete".localized, style: .destructive) { _ in
             deleteHandler?()
         }
         alert.addAction(deleteAction)
@@ -85,12 +85,12 @@ public extension UIViewController {
         }
         
         // 添加按钮
-        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
+        let cancelAction = UIAlertAction(title: "Cancel".localized, style: .cancel) { _ in
             cancelHandler?()
         }
         alert.addAction(cancelAction)
         
-        let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in
+        let deleteAction = UIAlertAction(title: "Delete".localized, style: .destructive) { _ in
             deleteHandler?()
         }
         alert.addAction(deleteAction)
@@ -102,3 +102,5 @@ public extension UIViewController {
         present(alert, animated: true, completion: nil)
     }
 }
+
+

+ 20 - 1
TSSmalCoacopods/Classes/GlobalImports/GlobalImports.swift

@@ -38,7 +38,7 @@ public func k_Height_safeAreaInsetsTop() -> CGFloat {
 }
 
 /// ③、底部安全区高度
-func k_Height_safeAreaInsetsBottom() -> CGFloat {
+public func k_Height_safeAreaInsetsBottom() -> CGFloat {
     if #available(iOS 13.0, *) {
         let scene = UIApplication.shared.connectedScenes.first;
         guard let windowScene = scene as? UIWindowScene else {return 0};
@@ -113,6 +113,10 @@ public func className(from object: Any) -> String {
     return String(describing: type(of: object))
 }
 
+public func appShortVersion() ->String{
+    let short = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
+    return short
+}
 
 public func appVersion() ->String{
     let short = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
@@ -146,3 +150,18 @@ public func kPushVC(target:UIViewController,modelVC:UIViewController){
     modelVC.hidesBottomBarWhenPushed = true
     target.navigationController?.pushViewController(modelVC, animated: true)
 }
+
+public func kGetVideoThumbnail(from videoURL: URL) -> UIImage? {
+    let asset = AVAsset(url: videoURL)
+    let assetImgGenerate = AVAssetImageGenerator(asset: asset)
+    assetImgGenerate.appliesPreferredTrackTransform = true
+    
+    let time = CMTimeMakeWithSeconds(0.0, preferredTimescale: 600) // 获取第0秒的帧
+    do {
+        let imgRef = try assetImgGenerate.copyCGImage(at: time, actualTime: nil)
+        return UIImage(cgImage: imgRef)
+    } catch {
+        print("Error generating thumbnail: \(error.localizedDescription)")
+        return nil
+    }
+}

+ 151 - 0
TSSmalCoacopods/Classes/Tool/TSBusinessFileManager.swift

@@ -0,0 +1,151 @@
+//
+//  TSBusinessFileManager.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/27.
+//
+
+class TSBusinessFileManager {
+    
+    /// 获取 Video 下载后保存的的文件件路径
+    static var saveVideoPathURL:URL = {
+        let saveRingPathURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("video")
+        return saveRingPathURL
+    }()
+    
+    static var saveCacheAllPathURL:URL = {
+        let saveRingPathURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("cacheAll")
+        return saveRingPathURL
+    }()
+    
+    public static func generateFileName(
+        urlString: String,
+        fileEx:String? = nil,
+    missingEx:String? = nil
+    )->String?{
+        guard let url = URL(string: urlString) else{
+            return nil
+        }
+        
+        var fileName = url.path.md5
+
+        // 使用 URL 的 MD5 哈希值作为缓存文件名,附加 URL 的后缀名
+        var fileExtension = ""
+        if let fileEx = fileEx {
+            fileExtension = fileEx
+        }else{
+            var missingExStr = ""
+            if let missingEx = missingEx {
+                missingExStr = missingEx
+            }
+            fileExtension = url.pathExtension.isEmpty ? missingExStr : url.pathExtension
+        }
+
+        if fileExtension.count > 0 {
+            fileName = url.path.md5 + ".\(fileExtension)"
+        }
+         
+        return fileName
+    }
+    
+    //获取 urlstring 本地的缓存 url path
+    public static func getLocalURL(
+        urlString: String,
+        fileEx:String? = nil,
+    missingEx:String? = nil)->URL?{
+        
+        guard let fileName = generateFileName(urlString: urlString,fileEx:fileEx,missingEx: missingEx) else{
+            return nil
+        }
+        
+        //检查文件是否已存在于缓存中
+        let ringFileURL = saveVideoPathURL.appendingPathComponent(fileName)
+        if FileManager.default.fileExists(atPath: ringFileURL.path) {
+            print("文件已存在于缓存中: \(ringFileURL)")
+            return ringFileURL
+        }
+        
+        let cachedFileURL = saveCacheAllPathURL.appendingPathComponent(fileName)
+        if FileManager.default.fileExists(atPath: cachedFileURL.path) {
+            print("文件已存在于缓存中: \(cachedFileURL)")
+            return cachedFileURL
+        }
+        
+        return nil
+    }
+    
+
+}
+
+//缓存路径
+extension TSBusinessFileManager {
+    
+    //检查 url 对不对
+    public static func generateCachesURL(
+        from urlString: String,
+        fileEx:String? = nil,
+    missingEx:String? = nil,
+        cacheDirectory:String = "cacheAll",
+        completion:((String?, Error?) -> Void)? = nil
+    )->URL?
+    {
+        guard let url = URL(string: urlString) else{
+            completion?(nil, NSError(domain: "url null", code: 0))
+            return nil
+        }
+        
+        if !urlString.contains("http") && urlString.contains("/"){
+            completion?(urlString.fillCachePath, nil)
+            return nil
+        }
+        
+        let fileManager = FileManager.default
+        
+        // 获取缓存目录下的 `cacheAll` 文件夹路径
+        let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
+        let cacheAllDirectory = cachesDirectory.appendingPathComponent(cacheDirectory)
+        
+        // 创建 `cacheAll` 文件夹(如果不存在)
+        if !fileManager.fileExists(atPath: cacheAllDirectory.path) {
+            do {
+                try fileManager.createDirectory(at: cacheAllDirectory, withIntermediateDirectories: true, attributes: nil)
+            } catch {
+                completion?(nil, error)
+                return nil
+            }
+        }
+        
+        
+        guard let fileName = generateFileName(urlString: urlString,fileEx:fileEx,missingEx: missingEx) else{
+            completion?(nil, NSError(domain: "url error", code: 0))
+            return nil
+        }
+
+        let cachedFileURL = cacheAllDirectory.appendingPathComponent(fileName)
+        return cachedFileURL
+    }
+    
+    //获取 urlstring 本地的缓存 url path
+    public static func getCachesURL(
+        from urlString: String,
+        fileEx:String? = nil,
+    missingEx:String? = nil,
+        cacheDirectory:String = "cacheAll")->URL?{
+        
+        if let cachedFileURL = generateCachesURL(
+            from: urlString,
+            fileEx: fileEx,
+            missingEx: missingEx,
+            cacheDirectory: cacheDirectory
+        ){
+            //检查文件是否已存在于缓存中
+            if FileManager.default.fileExists(atPath: cachedFileURL.path) {
+                print("文件已存在于缓存中: \(cachedFileURL)")
+                return cachedFileURL
+            }
+        }
+        
+        return nil
+    }
+    
+}

+ 25 - 3
TSSmalCoacopods/Classes/Tool/TSCommonTool/TSCommonTool+Down.swift

@@ -97,11 +97,11 @@ public extension TSCommonTool {
         
         let fileManager = FileManager.default
         
-        // 获取缓存目录下的 `cacheVideo` 文件夹路径
+        // 获取缓存目录下的 `cacheAll` 文件夹路径
         let cachesDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
         let cacheVideoDirectory = cachesDirectory.appendingPathComponent(cacheDirectory)
         
-        // 创建 `cacheVideo` 文件夹(如果不存在)
+        // 创建 `cacheAll` 文件夹(如果不存在)
         if !fileManager.fileExists(atPath: cacheVideoDirectory.path) {
             do {
                 try fileManager.createDirectory(at: cacheVideoDirectory, withIntermediateDirectories: true, attributes: nil)
@@ -117,7 +117,7 @@ public extension TSCommonTool {
         var fileExtension = ""
         
         if let fileEx = fileEx {
-            fileExtension = "fileEx"
+            fileExtension = fileEx
         }else{
             var missingExStr = ""
             if let missingEx = missingEx {
@@ -135,6 +135,28 @@ public extension TSCommonTool {
 
     }
     
+//    func a(){
+//        var fileName = url.path.md5
+//        
+//        // 使用 URL 的 MD5 哈希值作为缓存文件名,附加 URL 的后缀名
+//        var fileExtension = ""
+//        
+//        if let fileEx = fileEx {
+//            fileExtension = "fileEx"
+//        }else{
+//            var missingExStr = ""
+//            if let missingEx = missingEx {
+//                missingExStr = missingEx
+//            }
+//            fileExtension = url.pathExtension.isEmpty ? missingExStr : url.pathExtension
+//        }
+//
+//        if fileExtension.count > 0 {
+//            fileName = url.path.md5 + ".\(fileExtension)"
+//        }
+//
+//    }
+    
     //获取 urlstring 本地的缓存 url path
     public static func getCachedURLString(
         from urlString: String,

+ 82 - 82
TSSmalCoacopods/Classes/Tool/TSCommonTool/TSCommonTool+MultDown.swift

@@ -5,85 +5,85 @@
 //  Created by 100Years on 2025/3/17.
 //
 
-public extension TSCommonTool {
-    
-    /// 多任务下载并缓存文件,依据 URL 的后缀名动态设置文件名
-    /// - Parameters:
-    ///   - url: 文件的 URL 地址
-    ///   - completion: 完成回调,返回本地缓存路径或错误
-    public static func multidownloadAndCacheFile(
-        from urlString: String,
-        fileEx:String? = nil,
-     missingEx:String? = nil,
-        cacheDirectory:String = "cacheAll",
-        progressHandler: @escaping (Double) -> Void,
-        completion: @escaping (String?, Error?) -> Void) -> TSMultiTaskDownloader?
-    {
-        
-        guard let url = URL(string: urlString) else{
-            completion(nil, NSError(domain: "url null", code: 0))
-            return nil
-        }
-        
-    
-        guard let cachedFileURL = checkURLString(
-            from: urlString,
-            fileEx: fileEx,
-            missingEx: missingEx,
-            cacheDirectory: cacheDirectory,
-            completion: completion
-        ) else {
-            return nil
-        }
-        
-        let fileManager = FileManager.default
-        //检查文件是否已存在于缓存中
-        if fileManager.fileExists(atPath: cachedFileURL.path) {
-            print("文件已存在于缓存中: \(cachedFileURL)")
-            completion(cachedFileURL.path, nil)
-            return nil
-        }
-        
-        
-        let downloader = TSMultiTaskDownloader.shared
-        downloader.downloadFile(from: url, progressHandler: { progress in
-            print("Download progress for file1: \(progress * 100)%")
-            progressHandler(progress)
-        }, completionHandler: { tempFileURL, error in
-            if let error = error {
-                DispatchQueue.main.async {
-                    completion(nil, error)
-                }
-                return
-            }
-            
-            guard let tempFileURL = tempFileURL else {
-                DispatchQueue.main.async {
-                    completion(nil, NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "临时文件路径不存在"]))
-                }
-                return
-            }
-            
-            do {
-                if fileManager.fileExists(atPath: cachedFileURL.path) {
-                    try fileManager.removeItem(atPath:cachedFileURL.path)
-                    dePrint("下载成功,移除已有的旧文件: \(cachedFileURL)")
-                }
-                try fileManager.moveItem(at: tempFileURL, to: cachedFileURL)
-                dePrint("文件下载并缓存成功: \(cachedFileURL)")
-                DispatchQueue.main.async {
-                    completion(cachedFileURL.path, nil)
-                }
-            } catch {
-                dePrint("文件下载成功,但失败:\(error)")
-                DispatchQueue.main.async {
-                    completion(nil, error)
-                }
-            }
-        })
-        
-        return downloader
-    }
-    
-
-}
+//public extension TSCommonTool {
+//    
+//    /// 多任务下载并缓存文件,依据 URL 的后缀名动态设置文件名
+//    /// - Parameters:
+//    ///   - url: 文件的 URL 地址
+//    ///   - completion: 完成回调,返回本地缓存路径或错误
+//    public static func multidownloadAndCacheFile(
+//        from urlString: String,
+//        fileEx:String? = nil,
+//     missingEx:String? = nil,
+//        cacheDirectory:String = "cacheAll",
+//        progressHandler: @escaping (Double) -> Void,
+//        completion: @escaping (String?, Error?) -> Void) -> TSMultiTaskDownloader?
+//    {
+//        
+//        guard let url = URL(string: urlString) else{
+//            completion(nil, NSError(domain: "url null", code: 0))
+//            return nil
+//        }
+//        
+//    
+//        guard let cachedFileURL = checkURLString(
+//            from: urlString,
+//            fileEx: fileEx,
+//            missingEx: missingEx,
+//            cacheDirectory: cacheDirectory,
+//            completion: completion
+//        ) else {
+//            return nil
+//        }
+//        
+//        let fileManager = FileManager.default
+//        //检查文件是否已存在于缓存中
+//        if fileManager.fileExists(atPath: cachedFileURL.path) {
+//            print("文件已存在于缓存中: \(cachedFileURL)")
+//            completion(cachedFileURL.path, nil)
+//            return nil
+//        }
+//        
+//        
+//        let downloader = TSMultiTaskDownloader.shared
+//        downloader.downloadFile(from: url, progressHandler: { progress in
+//            print("Download progress for file1: \(progress * 100)%")
+//            progressHandler(progress)
+//        }, completionHandler: { tempFileURL, error in
+//            if let error = error {
+//                DispatchQueue.main.async {
+//                    completion(nil, error)
+//                }
+//                return
+//            }
+//            
+//            guard let tempFileURL = tempFileURL else {
+//                DispatchQueue.main.async {
+//                    completion(nil, NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "临时文件路径不存在"]))
+//                }
+//                return
+//            }
+//            
+//            do {
+//                if fileManager.fileExists(atPath: cachedFileURL.path) {
+//                    try fileManager.removeItem(atPath:cachedFileURL.path)
+//                    dePrint("下载成功,移除已有的旧文件: \(cachedFileURL)")
+//                }
+//                try fileManager.moveItem(at: tempFileURL, to: cachedFileURL)
+//                dePrint("文件下载并缓存成功: \(cachedFileURL)")
+//                DispatchQueue.main.async {
+//                    completion(cachedFileURL.path, nil)
+//                }
+//            } catch {
+//                dePrint("文件下载成功,但失败:\(error)")
+//                DispatchQueue.main.async {
+//                    completion(nil, error)
+//                }
+//            }
+//        })
+//        
+//        return downloader
+//    }
+//    
+//
+//}

+ 74 - 74
TSSmalCoacopods/Classes/Tool/TSCommonTool/TSMultiTaskDownloader.swift

@@ -7,77 +7,77 @@
 
 import Foundation
 
-public class TSMultiTaskDownloader: NSObject, URLSessionDownloadDelegate {
-    
-    public static let shared = TSMultiTaskDownloader()
-    
-    private var session: URLSession!
-    private var downloadTasks: [URLSessionDownloadTask] = []
-    private var progressHandlers: [URL: (Double) -> Void] = [:]
-    private var completionHandlers: [URL: (URL?, Error?) -> Void] = [:]
-    
-    override init() {
-        super.init()
-        let config = URLSessionConfiguration.background(withIdentifier: "com.yourapp.multitaskdownloader")
-        session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
-    }
-    
-    public func downloadFile(from url: URL, progressHandler: @escaping (Double) -> Void, completionHandler: @escaping (URL?, Error?) -> Void) {
-        let downloadTask = session.downloadTask(with: url)
-        downloadTasks.append(downloadTask)
-        progressHandlers[url] = progressHandler
-        completionHandlers[url] = completionHandler
-        downloadTask.resume()
-    }
-    
-    public func cancelDownload(for url: URL) {
-        if let task = downloadTasks.first(where: { $0.originalRequest?.url == url }) {
-            task.cancel()
-            downloadTasks.removeAll { $0.originalRequest?.url == url }
-            progressHandlers.removeValue(forKey: url)
-            completionHandlers.removeValue(forKey: url)
-        }
-    }
-    
-    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
-        guard let originalURL = downloadTask.originalRequest?.url else { return }
-        
-        let fileManager = FileManager.default
-        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
-        let destinationURL = documentsDirectory.appendingPathComponent(originalURL.lastPathComponent)
-        
-        do {
-            try fileManager.moveItem(at: location, to: destinationURL)
-            completionHandlers[originalURL]?(destinationURL, nil)
-        } catch {
-            completionHandlers[originalURL]?(nil, error)
-        }
-        
-        downloadTasks.removeAll { $0.originalRequest?.url == originalURL }
-        progressHandlers.removeValue(forKey: originalURL)
-        completionHandlers.removeValue(forKey: originalURL)
-    }
-    
-    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
-        guard let originalURL = downloadTask.originalRequest?.url else { return }
-        
-        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
-        progressHandlers[originalURL]?(progress)
-    }
-    
-    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
-        guard let originalURL = task.originalRequest?.url else { return }
-        
-        if let error = error {
-            completionHandlers[originalURL]?(nil, error)
-        }
-        
-        downloadTasks.removeAll { $0.originalRequest?.url == originalURL }
-        progressHandlers.removeValue(forKey: originalURL)
-        completionHandlers.removeValue(forKey: originalURL)
-    }
-    
-    deinit {
-        session.invalidateAndCancel()
-    }
-}
+//public class TSMultiTaskDownloader: NSObject, URLSessionDownloadDelegate {
+//    
+//    public static let shared = TSMultiTaskDownloader()
+//    
+//    private var session: URLSession!
+//    private var downloadTasks: [URLSessionDownloadTask] = []
+//    private var progressHandlers: [URL: (Double) -> Void] = [:]
+//    private var completionHandlers: [URL: (URL?, Error?) -> Void] = [:]
+//    
+//    override init() {
+//        super.init()
+//        let config = URLSessionConfiguration.background(withIdentifier: "com.yourapp.multitaskdownloader")
+//        session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
+//    }
+//    
+//    public func downloadFile(from url: URL, progressHandler: @escaping (Double) -> Void, completionHandler: @escaping (URL?, Error?) -> Void) {
+//        let downloadTask = session.downloadTask(with: url)
+//        downloadTasks.append(downloadTask)
+//        progressHandlers[url] = progressHandler
+//        completionHandlers[url] = completionHandler
+//        downloadTask.resume()
+//    }
+//    
+//    public func cancelDownload(for url: URL) {
+//        if let task = downloadTasks.first(where: { $0.originalRequest?.url == url }) {
+//            task.cancel()
+//            downloadTasks.removeAll { $0.originalRequest?.url == url }
+//            progressHandlers.removeValue(forKey: url)
+//            completionHandlers.removeValue(forKey: url)
+//        }
+//    }
+//    
+//    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+//        guard let originalURL = downloadTask.originalRequest?.url else { return }
+//        
+//        let fileManager = FileManager.default
+//        let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+//        let destinationURL = documentsDirectory.appendingPathComponent(originalURL.lastPathComponent)
+//        
+//        do {
+//            try fileManager.moveItem(at: location, to: destinationURL)
+//            completionHandlers[originalURL]?(destinationURL, nil)
+//        } catch {
+//            completionHandlers[originalURL]?(nil, error)
+//        }
+//        
+//        downloadTasks.removeAll { $0.originalRequest?.url == originalURL }
+//        progressHandlers.removeValue(forKey: originalURL)
+//        completionHandlers.removeValue(forKey: originalURL)
+//    }
+//    
+//    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+//        guard let originalURL = downloadTask.originalRequest?.url else { return }
+//        
+//        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
+//        progressHandlers[originalURL]?(progress)
+//    }
+//    
+//    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+//        guard let originalURL = task.originalRequest?.url else { return }
+//        
+//        if let error = error {
+//            completionHandlers[originalURL]?(nil, error)
+//        }
+//        
+//        downloadTasks.removeAll { $0.originalRequest?.url == originalURL }
+//        progressHandlers.removeValue(forKey: originalURL)
+//        completionHandlers.removeValue(forKey: originalURL)
+//    }
+//    
+//    deinit {
+//        session.invalidateAndCancel()
+//    }
+//}

+ 76 - 0
TSSmalCoacopods/Classes/Tool/TSCustomAlertController.swift

@@ -0,0 +1,76 @@
+//
+//  TSCustomAlertController.swift
+//  Pods
+//
+//  Created by 100Years on 2025/3/26.
+//
+
+import UIKit
+
+
+public class TSCustomAlertController {
+    
+    // MARK: - 配置模型
+    public struct AlertConfig {
+        public var style: UIUserInterfaceStyle = .dark
+        public var message: String
+        public var messageColor: UIColor = .black
+        public var messageFont: UIFont = .systemFont(ofSize: 14)
+        
+        public var cancelTitle: String = "取消"
+        public var cancelColor: UIColor = .gray
+        
+        public var confirmTitle: String = "确定"
+        public var confirmColor: UIColor = .blue
+        
+        public var cancelAction: (() -> Void)?
+        public var confirmAction: (() -> Void)?
+        
+        public init(style:UIUserInterfaceStyle = .dark, message: String, messageColor: UIColor, messageFont: UIFont, cancelTitle: String, cancelColor: UIColor, confirmTitle: String, confirmColor: UIColor, cancelAction: ( () -> Void)? = nil, confirmAction: ( () -> Void)? = nil) {
+            self.style = style
+            self.message = message
+            self.messageColor = messageColor
+            self.messageFont = messageFont
+            self.cancelTitle = cancelTitle
+            self.cancelColor = cancelColor
+            self.confirmTitle = confirmTitle
+            self.confirmColor = confirmColor
+            self.cancelAction = cancelAction
+            self.confirmAction = confirmAction
+        }
+    }
+    
+    // MARK: - 显示弹窗
+    public static func show(in viewController: UIViewController, config: AlertConfig) {
+        // 1. 创建AlertController (title设为空字符串而不是nil,避免自动上移)
+        let alert = UIAlertController(title: "", message: "", preferredStyle: .alert)
+        
+        // 2. 自定义message
+        let messageAttributed = NSAttributedString(
+            string: config.message,
+            attributes: [
+                .foregroundColor: config.messageColor,
+                .font: config.messageFont
+            ]
+        )
+        alert.setValue(messageAttributed, forKey: "attributedMessage")
+        // 设置黑暗模式
+        alert.overrideUserInterfaceStyle = config.style
+        // 3. 添加取消按钮
+        let cancelAction = UIAlertAction(title: config.cancelTitle, style: .default) { _ in
+            config.cancelAction?()
+        }
+        cancelAction.setValue(config.cancelColor, forKey: "titleTextColor")
+        alert.addAction(cancelAction)
+        
+        // 4. 添加确定按钮
+        let confirmAction = UIAlertAction(title: config.confirmTitle, style: .default) { _ in
+            config.confirmAction?()
+        }
+        confirmAction.setValue(config.confirmColor, forKey: "titleTextColor")
+        alert.addAction(confirmAction)
+        
+        // 5. 显示弹窗
+        viewController.present(alert, animated: true)
+    }
+}

+ 111 - 0
TSSmalCoacopods/Classes/Tool/TSDownloadManager.swift

@@ -0,0 +1,111 @@
+//
+//  TSPublicContent.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/20.
+//
+
+
+import AVFoundation
+import Alamofire
+
+class TSDownloadManager {
+    
+
+    static func downloadFile(
+        urlString: String,
+        fileEx:String? = nil,
+    missingEx:String? = nil,
+        progressHandler: ((Double) -> Void)? = nil,
+        completion: @escaping (URL?, Error?) -> Void
+    ) -> DownloadRequest? {
+        
+        if let fileName = TSBusinessFileManager.getLocalURL(urlString: urlString,fileEx:fileEx,missingEx:missingEx){
+            completion(fileName,nil)
+            return nil
+        }
+    
+        guard let cachesAllPath = TSBusinessFileManager.generateCachesURL(from: urlString, fileEx: fileEx, missingEx: missingEx,completion: { string, error in
+            completion(nil,error)
+        })else { return nil }
+        
+
+        return nil
+//        return TSNetworkShared.downloadFile(urlString: urlString,to: cachesAllPath, progressHandler:progressHandler,completion: completion)
+    }
+    
+//
+//    
+//    
+//    static func getDownLoadRing(ringModel:TSRingModel,complete:@escaping (URL?,Bool)->Void){
+//        let urlString = ringModel.audioUrl
+//        if let path = TSDownloadManager.getRingLocalURL(ringModel: ringModel) {
+//            complete(path,false)
+//        }else{
+//            _ = TSDownloadManager.downloadFile(urlString:urlString,missingEx: "mp3") { url, error in
+//                if let path = url {
+//                    complete(path,true)
+//                }else{
+//                    complete(nil,true)
+//                }
+//            }
+//        }
+//    }
+    
+//    static func getDownLoadVideo(urlString:String,complete:@escaping (URL?,Bool)->Void){
+//        if let path = TSDownloadManager.getRingLocalURL(urlString: urlString) {
+//            complete(path,false)
+//        }else{
+//            _ = TSDownloadManager.downloadFile(urlString:urlString,missingEx: "mp3") { url, error in
+//                if let path = url {
+//                    complete(path,true)
+//                }else{
+//                    complete(nil,true)
+//                }
+//            }
+//        }
+//    }
+
+}
+//
+//extension TSDownloadManager {
+//    
+//    
+//    //获取 TSRingModel 本地的缓存
+//    public static func getRingLocalURL(ringModel: TSRingModel)->URL?{
+//        if ringModel.documentPath.count == 0 {
+//            return nil
+//        }
+//        
+//        let documentPath = ringModel.documentPath.fillDocumentURL
+//        if TSFileManagerTool.fileExists(at: documentPath) {
+//            return documentPath
+//        }
+//        
+//        let urlString = ringModel.audioUrl
+//        guard let fileName = TSBusinessFileManager.getLocalURL(urlString: urlString,fileEx:nil,missingEx: "mp3") else{
+//            return nil
+//        }
+//        return fileName
+//    }
+//    
+//    //获取 urlstring 本地的缓存 url path
+//    public static func getRingLocalURL(urlString: String)->URL?{
+//        guard let fileName = TSBusinessFileManager.getLocalURL(urlString: urlString,fileEx:nil,missingEx: "mp3") else{
+//            return nil
+//        }
+//        return fileName
+//    }
+//
+//    //获取 urlstring 本地的缓存 url path
+//    public static func generateRingSaveLocalURL(name:String,timestamp:Bool = true,fileExtension:String = "mp3")->URL{
+//        var fileName = name
+//        if timestamp {
+//            fileName = fileName + String(Date.timestampInt)
+//        }
+//        
+//        fileName = fileName + "." + fileExtension
+//        return TSBusinessFileManager.saveRingPathURL.appendingPathComponent(fileName)
+//    }
+//    
+//}

+ 50 - 34
TSSmalCoacopods/Classes/Tool/TSFileManagerTool.swift

@@ -5,43 +5,24 @@
 //  Created by 100Years on 2024/12/26.
 //
 
-class TSFileManagerTool {
-    
-    /// 获取 Video 下载后保存的的文件件路径
-    static var saveDownVideoPathURL:URL = {
-        let saveVideoPathURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("livePhoto").appendingPathComponent("saveVideo")
-        return saveVideoPathURL
-    }()
-    
-    /// 获取 Video 临时编辑的文件件路径
-    static var editLiveVideoPathURL:URL = {
-        let editVideoPathURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("livePhoto").appendingPathComponent("editVideo")
-        return editVideoPathURL
-    }()
-    
-    /// 获取 Video 编辑后保存的的文件件路径
-    static var saveLiveVideoPathURL:URL = {
-        let saveVideoPathURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("livePhoto").appendingPathComponent("saveVideo")
-        return saveVideoPathURL
-    }()
-    
+public class TSFileManagerTool {
 
     /// 获取沙盒 Documents 目录路径
-    static var documentsDirectory: URL {
+    public static var documentsDirectory: URL {
         return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
     }
 
     /// 获取沙盒 Cache 目录路径
-    static var cacheDirectory: URL {
+    public static var cacheDirectory: URL {
         return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
     }
 
     /// 获取沙盒 Temporary 目录路径
-    static var temporaryDirectory: URL {
+    public static var temporaryDirectory: URL {
         return FileManager.default.temporaryDirectory
     }
 
-    static func copyFileWithOverwrite(from sourceURL: URL, to targetURL: URL) {
+    public  static func copyFileWithOverwrite(from sourceURL: URL, to targetURL: URL) {
         let fileManager = FileManager.default
         do {
             removeItem(from: targetURL)
@@ -53,7 +34,7 @@ class TSFileManagerTool {
         }
     }
     
-    static func removeItem(from sourceURL: URL) {
+    public static func removeItem(from sourceURL: URL) {
         let fileManager = FileManager.default
         do {
             // 如果目标路径存在同名文件,先删除旧文件
@@ -71,7 +52,7 @@ class TSFileManagerTool {
     ///   - sourceURL: 文件的源 URL
     ///   - destinationURL: 目标 URL
     /// - Throws: 如果移动失败,会抛出错误
-    static func moveFile(from sourceURL: URL, to destinationURL: URL) {
+    public static func moveFile(from sourceURL: URL, to destinationURL: URL) {
         let fileManager = FileManager.default
         
         // 检查源文件是否存在
@@ -102,7 +83,7 @@ class TSFileManagerTool {
         }
     }
     
-    static func getFileName(from url: URL, includeExtension: Bool = true) -> String {
+    public static func getFileName(from url: URL, includeExtension: Bool = true) -> String {
         if includeExtension {
             return url.lastPathComponent
         } else {
@@ -110,7 +91,7 @@ class TSFileManagerTool {
         }
     }
     
-    static func checkFolderAndCreate(from destinationURL: URL){
+    public static func checkFolderAndCreate(from destinationURL: URL){
         let fileManager = FileManager.default
         let destinationDirectory = destinationURL.deletingLastPathComponent()
         // 如果目标文件夹不存在,创建文件夹
@@ -126,19 +107,19 @@ class TSFileManagerTool {
     // MARK: - 文件操作方法
 
     /// 检查文件或文件夹是否存在
-    static func fileExists(at url: URL) -> Bool {
+    public static func fileExists(at url: URL) -> Bool {
         return FileManager.default.fileExists(atPath: url.path)
     }
 
     /// 创建文件夹
-    static func createDirectory(at url: URL) throws {
+    public static func createDirectory(at url: URL) throws {
         if !fileExists(at: url) {
             try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
         }
     }
     
     //获取缓存目录下文件夹路径
-    static func getCacheSubPath(at url: URL) ->String? {
+    public static func getCacheSubPath(at url: URL) ->String? {
         let array = url.path.components(separatedBy:"/Caches/")
         let cashFilePath = array.last
         return cashFilePath
@@ -146,12 +127,47 @@ class TSFileManagerTool {
     
 }
 
-extension String {
-    var fillCachePath:String{
+public extension String {
+    
+    public var isCachesPath:Bool{
+        return self.contains("/Caches/")
+    }
+    
+    public var fillCachePath:String{
         return TSFileManagerTool.cacheDirectory.appendingPathComponent(self).path
     }
     
-    var fillCacheURL:URL{
+    public var fillCacheURL:URL{
         return TSFileManagerTool.cacheDirectory.appendingPathComponent(self)
     }
+    
+    public var cachesLastURLString:String{
+        let parts = self.components(separatedBy: "/Caches/")
+        if let last = parts.last {
+            return last
+        }
+        return self
+    }
+}
+public extension String {
+    
+    public var isDocumentPath:Bool{
+        return self.contains("/Documents/")
+    }
+    
+    public var fillDocumentPath:String{
+        return TSFileManagerTool.documentsDirectory.appendingPathComponent(self).path
+    }
+    
+    public var fillDocumentURL:URL{
+        return TSFileManagerTool.documentsDirectory.appendingPathComponent(self)
+    }
+    
+    public var documentLastURLString:String{
+        let parts = self.components(separatedBy: "/Documents/")
+        if let last = parts.last {
+            return last
+        }
+        return self
+    }
 }

+ 190 - 0
TSSmalCoacopods/Classes/Tool/TSImageCompress.swift

@@ -0,0 +1,190 @@
+//
+//  TSImageCompress.swift
+//  Pods
+//
+//  Created by 100Years on 2025/4/15.
+//
+
+import UIKit
+
+// MARK: - UIImage 扩展(Alpha 通道检测和缩放)
+public extension UIImage {
+    /// 检查图片是否有 Alpha 通道(透明度)
+    func hasAlphaChannel() -> Bool {
+        guard let cgImage = self.cgImage else { return false }
+        let alphaInfo = cgImage.alphaInfo
+        return alphaInfo == .first || alphaInfo == .last || alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast
+    }
+    
+    /// 按比例缩放图片
+    func resized(withScaleFactor scaleFactor: CGFloat) -> UIImage {
+        let newSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
+        let renderer = UIGraphicsImageRenderer(size: newSize)
+        return renderer.image { _ in
+            self.draw(in: CGRect(origin: .zero, size: newSize))
+        }
+    }
+}
+
+public class TSImageCompress {
+
+    // MARK: - 核心压缩方法
+    public static func compressImageToTargetSize(
+        _ image: UIImage,
+        targetSizeKB: Int,
+        preserveTransparency: Bool
+    ) -> Data? {
+        let targetBytes = targetSizeKB * 1024
+        let hasAlpha = image.hasAlphaChannel()
+        let usePNG = preserveTransparency && hasAlpha
+
+        if usePNG {
+            let compressPNGImage = compressPNGImage(image, targetBytes: targetBytes)
+            if let compressedData = compressPNGImage {
+//                saveData(compressedData: compressedData)
+                print("PNG 压缩后大小: \(compressedData.count / 1024)KB")
+            }
+            return compressPNGImage
+        } else {
+            let compressJPEGImage = compressJPEGImage(image, targetBytes: targetBytes)
+            if let compressJPEGImage = compressJPEGImage {
+//                saveData(compressedData: compressJPEGImage)
+                print("JPEG 压缩后大小: \(compressJPEGImage.count / 1024)KB")
+            }
+            return compressJPEGImage
+        }
+    }
+
+    // MARK: - PNG 压缩逻辑(调整尺寸)
+    static private func compressPNGImage(_ image: UIImage, targetBytes: Int) -> Data? {
+        var currentImage = image
+        var currentData = currentImage.pngData()
+        var currentBytes = currentData?.count ?? 0
+        
+        // 如果已经满足目标大小,直接返回
+        guard currentBytes > targetBytes else { return currentData }
+        
+        var scaleFactor: CGFloat = 0.9
+        let minScaleFactor: CGFloat = 0.1
+        
+        while scaleFactor >= minScaleFactor {
+            let scaledImage = currentImage.resized(withScaleFactor: scaleFactor)
+            guard let newData = scaledImage.pngData() else { break }
+            let newBytes = newData.count
+            
+            if newBytes <= targetBytes {
+                return newData // 达到目标
+            }
+            
+            if newBytes >= currentBytes {
+                break // 无法继续优化
+            }
+            
+            currentImage = scaledImage
+            currentData = newData
+            currentBytes = newBytes
+            scaleFactor -= 0.1
+        }
+        
+        return currentBytes <= targetBytes ? currentData : nil
+    }
+
+    // MARK: - JPEG 压缩逻辑(先调质量,后调尺寸)
+    static private func compressJPEGImage(_ image: UIImage, targetBytes: Int) -> Data? {
+        var compressionQuality: CGFloat = 1.0
+        var currentImage = image
+        var currentData = currentImage.jpegData(compressionQuality: compressionQuality)
+        var currentBytes = currentData?.count ?? 0
+        
+        // 第一步:降低 JPEG 质量
+        while currentBytes > targetBytes && compressionQuality > 0.1 {
+            compressionQuality -= 0.1
+            currentData = currentImage.jpegData(compressionQuality: compressionQuality)
+            currentBytes = currentData?.count ?? 0
+        }
+        
+        guard currentBytes > targetBytes else { return currentData }
+        
+        // 第二步:缩小尺寸
+        var scaleFactor: CGFloat = 0.9
+        let minScaleFactor: CGFloat = 0.1
+        
+        while scaleFactor >= minScaleFactor {
+            let scaledImage = currentImage.resized(withScaleFactor: scaleFactor)
+            guard let newData = scaledImage.jpegData(compressionQuality: compressionQuality) else { break }
+            let newBytes = newData.count
+            
+            if newBytes <= targetBytes {
+                return newData // 达到目标
+            }
+            
+            if newBytes >= currentBytes {
+                break // 无法继续优化
+            }
+            
+            currentImage = scaledImage
+            currentData = newData
+            currentBytes = newBytes
+            scaleFactor -= 0.1
+        }
+        
+        return currentBytes <= targetBytes ? currentData : nil
+    }
+}
+
+
+extension TSImageCompress {
+    
+    static func saveData(compressedData:Data){
+        // 写入到沙盒的 "Images" 子目录
+          let fileName = "compressed_\(Date().timeIntervalSince1970).jpg"
+          if let fileURL = writeDataToSandbox(data: compressedData, fileName: fileName, subDirectory: "Images") {
+              print("文件已保存至: \(fileURL.path)")
+              
+              // 可选:读取文件验证
+              if let savedData = try? Data(contentsOf: fileURL) {
+                  print("读取文件大小: \(savedData.count / 1024)KB")
+              }
+          }
+    }
+    
+    static func getDocumentsDirectory() -> URL {
+        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
+        return paths[0] // 返回第一个路径(通常唯一)
+    }
+    
+    static func writeDataToSandbox(data: Data, fileName: String, subDirectory: String? = nil) -> URL? {
+        let baseDirectory: URL
+        
+        // 1. 确定基础目录(默认 Documents)
+        baseDirectory = getDocumentsDirectory()
+        
+        // 2. 拼接子目录(如果存在)
+        var targetDirectory = baseDirectory
+        if let subDir = subDirectory {
+            targetDirectory = baseDirectory.appendingPathComponent(subDir)
+        }
+        
+        // 3. 创建子目录(如果不存在)
+        do {
+            try FileManager.default.createDirectory(at: targetDirectory, withIntermediateDirectories: true, attributes: nil)
+        } catch {
+            print("创建目录失败: \(error.localizedDescription)")
+            return nil
+        }
+        
+        // 4. 拼接最终文件路径
+        let fileURL = targetDirectory.appendingPathComponent(fileName)
+        
+        // 5. 写入数据
+        do {
+            try data.write(to: fileURL)
+//            print("文件写入成功: \(fileURL.path)")
+            return fileURL
+        } catch {
+            print("写入文件失败: \(error.localizedDescription)")
+            return nil
+        }
+    }
+
+}

+ 52 - 8
TSSmalCoacopods/Classes/View/TSPlaceholderTextView/TSPlaceholderTextView.swift

@@ -7,6 +7,17 @@
 
 open class TSPlaceholderTextView: UITextView {
     
+//    // MARK: - 垂直对齐模式
+//    public enum VerticalAlignment {
+//        case top
+//        case center
+//        case bottom
+//    }
+//    
+//    public var verticalAlignment: VerticalAlignment = .top {
+//        didSet { setNeedsLayout() }
+//    }
+    
     // Placeholder Label
     public lazy var placeholderLabel: TopLeftLabel = {
         let placeholderLabel = TopLeftLabel()
@@ -105,7 +116,6 @@ open class TSPlaceholderTextView: UITextView {
     
     
     @objc private func textDidChange(_ notification: Notification) {
-       
         if let textView = notification.object as? UITextView {
             handleTextViewTextCount(textView: textView)
             textViewTextChanged?()
@@ -130,21 +140,55 @@ open class TSPlaceholderTextView: UITextView {
     open override func layoutSubviews() {
         super.layoutSubviews()
 
-        
-        
+  
+//        let textInsets = textCntainerInset
         let placeholderX = textInsets.left + 5 // Add padding for placeholder
         let placeholderY = textInsets.top
         let placeholderWidth = bounds.width - textInsets.left - textInsets.right - 10 // Adjust for padding
         let placeholderHeight = bounds.height - textInsets.top - textInsets.bottom
         
-        
-//        if let font = font {
-//            placeholderHeight = placeholder?.height(ofFont: font, maxWidth: k_ScreenWidth-64)
-//        }
-        
         placeholderLabel.frame = CGRect(x: placeholderX,
                                         y: placeholderY,
                                         width: placeholderWidth,
                                         height: placeholderHeight)
+        
+//        handleVerticalAlignment()
     }
+    
+    func handleVerticalAlignment() {
+      
+//          guard !bounds.isEmpty else { return }
+//          
+//        
+//        let label = UILabel()
+//        label.text = text
+//        label.font = font
+//        label.numberOfLines = 0
+//        var height = label.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude)).height
+//
+//        
+////          let size = sizeThatFits(CGSize(
+////              width: bounds.width,
+////              height: CGFloat.greatestFiniteMagnitude
+////          ))
+//          
+//          let topInset: CGFloat
+//          switch verticalAlignment {
+//          case .top:
+//              topInset = 0
+//          case .center:
+//              topInset = max(0, (bounds.height - height) / 2)
+//          case .bottom:
+//              topInset = max(0, bounds.height - height)
+//          }
+//          
+//          textContainerInset = UIEdgeInsets(
+//              top: topInset,
+//              left: textContainerInset.left,
+//              bottom: textContainerInset.bottom,
+//              right: textContainerInset.right
+//          )
+      }
 }
+
+

+ 68 - 0
TSSmalCoacopods/Classes/View/TSProgressSlider/TSProgressSlider.swift

@@ -0,0 +1,68 @@
+//
+//  TSProgressSlider.swift
+//  Pods
+//
+//  Created by 100Years on 2025/4/22.
+//
+
+import UIKit
+
+open class TSProgressSlider: UISlider {
+    // 透明覆盖视图
+    private let overlayView = UIView()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupOverlay()
+    }
+    
+    required public init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupOverlay()
+    }
+    
+    private func setupOverlay() {
+        // 配置透明覆盖视图
+        overlayView.backgroundColor = .clear
+        overlayView.isUserInteractionEnabled = true
+        addSubview(overlayView)
+        
+        // 添加拖动手势
+        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
+        overlayView.addGestureRecognizer(panGesture)
+        
+        // 添加点击手势
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
+        overlayView.addGestureRecognizer(tapGesture)
+    }
+    
+    open override func layoutSubviews() {
+        super.layoutSubviews()
+        // 确保覆盖视图与slider同大小
+        overlayView.frame = bounds
+    }
+    
+    // 处理拖动手势
+    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
+        let location = gesture.location(in: overlayView)
+        updateSliderValue(with: location)
+        
+        if gesture.state == .ended {
+            sendActions(for: .touchUpInside)
+        }
+    }
+    
+    // 处理点击手势
+    @objc private func handleTap(_ gesture: UITapGestureRecognizer) {
+        let location = gesture.location(in: overlayView)
+        updateSliderValue(with: location)
+        sendActions(for: .touchUpInside)
+    }
+    
+    // 更新滑块值
+    private func updateSliderValue(with location: CGPoint) {
+        let percentage = max(0, min(1, location.x / bounds.width))
+        setValue(Float(percentage), animated: false)
+        sendActions(for: .valueChanged)
+    }
+}

+ 186 - 0
TSSmalCoacopods/Classes/View/TSReusableCollectionView/TSSimpleCollectionView.swift

@@ -0,0 +1,186 @@
+//
+//  TSReusableCollectionView.swift
+//  Pods
+//
+//  Created by 100Years on 2025/4/25.
+//
+import UIKit
+
+// 单元格配置协议
+public protocol TSSimpleConfigurableView {
+    var data:Any? { get set }
+    var delegate:TSSimpleCollectionViewDelegate? { get set }
+    var indexPath:IndexPath? { get set }
+}
+
+// 单元格事件类型
+public enum TSSimpleCellAction {
+    case tap
+    case buttonTapped(String) // 支持自定义按钮标识
+    case custom(String)
+}
+
+// 回调参数结构体
+public struct TSSimpleCellEvent {
+    public let action: TSSimpleCellAction
+    public let indexPath: IndexPath
+    public let data: Any?
+    
+    public init(action: TSSimpleCellAction, indexPath: IndexPath, data: Any) {
+        self.action = action
+        self.indexPath = indexPath
+        self.data = data
+    }
+}
+
+// 集合视图回调协议
+public protocol TSSimpleCollectionViewDelegate: AnyObject {
+    func collectionView(didTrigger event: TSSimpleCellEvent)
+}
+
+
+public class TSSimpleSectionData {
+    public var title: String = ""
+    public var items: [Any] = []
+    
+    public init(
+        title:String = "",
+        items:[Any] = []
+    ) {
+        self.title = title
+        self.items = items
+    }
+}
+
+open class TSSimpleCollectionView: UIView {
+    
+    public lazy var layout: UICollectionViewFlowLayout = {
+        let layout = UICollectionViewFlowLayout()
+        layout.scrollDirection = .vertical
+        return layout
+    }()
+    
+    // MARK: - Properties
+    public  lazy var collectionView: UICollectionView = {
+        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
+        // 禁用自动 contentInset 调整
+        if #available(iOS 11.0, *) {
+            cv.contentInsetAdjustmentBehavior = .never
+        }
+        cv.backgroundColor = .clear
+        cv.delegate = self
+        cv.dataSource = self
+        return cv
+    }()
+    
+    public var sections: [TSSimpleSectionData] = []
+    public weak var delegate: TSSimpleCollectionViewDelegate?
+    
+    public var cellTypes: [String: (UIView & TSSimpleConfigurableView).Type] = [:]
+    public var cellIdentifierForItem: ((Any) -> String)?
+    // MARK: - Initialization
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupViews()
+    }
+    
+    required public init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupViews()
+    }
+    
+    private func setupViews() {
+        addSubview(collectionView)
+        collectionView.translatesAutoresizingMaskIntoConstraints = false
+        NSLayoutConstraint.activate([
+            collectionView.topAnchor.constraint(equalTo: topAnchor),
+            collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
+            collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
+            collectionView.bottomAnchor.constraint(equalTo: bottomAnchor)
+        ])
+    }
+    
+    // MARK: - Public Methods
+    public func registerCell<Cell: UIView & TSSimpleConfigurableView>(_ cellType: Cell.Type, identifier: String) {
+        cellTypes[identifier] = cellType
+        collectionView.register(cellType, forCellWithReuseIdentifier: identifier)
+    }
+    
+    public func registerSectionHeader<Cell: UIView & TSSimpleConfigurableView>(_ cellType: Cell.Type, identifier: String) {
+        cellTypes[identifier] = cellType
+        collectionView.register(cellType, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: identifier)
+    }
+    
+    public func registerSectionFooter<Cell: UIView & TSSimpleConfigurableView>(_ cellType: Cell.Type, identifier: String) {
+        cellTypes[identifier] = cellType
+        collectionView.register(cellType, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: identifier)
+    }
+    
+    public func reload(with sections: [TSSimpleSectionData]) {
+        self.sections = sections
+        collectionView.reloadData()
+    }
+    
+    public func reloadItem(at indexPath: IndexPath) {
+        collectionView.reloadItems(at: [indexPath])
+    }
+    
+    public func reloadSection(_ section: Int) {
+        collectionView.reloadSections(IndexSet(integer: section))
+    }
+}
+
+// MARK: - UICollectionViewDataSource & Delegate
+extension TSSimpleCollectionView: UICollectionViewDataSource, UICollectionViewDelegate {
+    public func numberOfSections(in collectionView: UICollectionView) -> Int {
+        return sections.count
+    }
+    
+    public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        return sections[section].items.count
+    }
+    
+    public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        let data = sections[indexPath.section].items[indexPath.item]
+        let identifier = cellIdentifierForItem?(data) ?? String(describing: type(of: data))
+        
+        guard let cellType = cellTypes[identifier] else {
+            fatalError("未注册对应数据类型的单元格: \(identifier)")
+        }
+        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
+       
+        if var configurableCell = cell as? (UICollectionViewCell & TSSimpleConfigurableView) {
+            
+            configurableCell.data = data
+            weak var delegate: TSSimpleCollectionViewDelegate? = delegate
+            configurableCell.delegate = delegate
+            configurableCell.indexPath = indexPath
+        }
+        
+        return cell
+    }
+    
+    public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        let data = sections[indexPath.section].items[indexPath.item]
+        let event = TSSimpleCellEvent(action: .tap, indexPath: indexPath, data: data)
+        delegate?.collectionView(didTrigger: event)
+    }
+    
+    
+    public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
+        let data = sections[indexPath.section]
+        let identifier = cellIdentifierForItem?(data) ?? String(describing: type(of: data))
+        
+        guard let cellType = cellTypes[identifier] else {
+            fatalError("未注册对应数据类型的区间头尾: \(identifier)")
+        }
+        
+        let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: identifier, for: indexPath)
+        if var configurableCell = reusableView as? (UICollectionViewCell & TSSimpleConfigurableView) {
+            configurableCell.data = data
+            configurableCell.delegate = delegate
+            configurableCell.indexPath = indexPath
+        }
+        return reusableView
+    }
+}

+ 120 - 0
TSSmalCoacopods/Classes/View/TSSaveSuccessTool/TSSaveSuccessTool.swift

@@ -0,0 +1,120 @@
+//
+//  TSSaveSuccessTool.swift
+//  Pods
+//
+//  Created by 100Years on 2025/4/24.
+//
+
+public let kSaveSuccesswShared = TSSaveSuccessTool.shared
+open class TSSaveSuccessTool {
+    
+    static let shared = TSSaveSuccessTool()
+    
+    public var clickViewHandle:(()->Void)?
+    
+    private lazy var textLabel:UILabel = {
+        let textLabel = UILabel()
+        textLabel.textColor = UIColor.white
+        textLabel.text = "Save Successfully".localized
+        textLabel.font = UIFont.font(size: 14)
+        return textLabel
+    }()
+    
+    private lazy var saveSuccessBg: UIView = {
+        return creatSaveSuccessBg()
+    }()
+    
+    
+    private lazy var viewButton:UIView = {
+        let color = "4FEA9D".uiColor
+        let viewButton = UIButton.createButton(title: "View".localized ,backgroundColor: color.withAlphaComponent(0.1),font: UIFont.font(size: 14),titleColor: color,corner: 14) { [weak self]  in
+            guard let self = self else { return }
+            if let clickViewHandle = clickViewHandle {
+                clickViewHandle()
+            }else {
+                if let url = URL(string: "photos-redirect://") {
+                    if UIApplication.shared.canOpenURL(url) {
+                        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+                        playVibration()
+                    }
+                }
+            }
+        }
+        return viewButton
+    }()
+    
+    func creatSaveSuccessBg() -> UIView {
+        let view = UIView()
+        view.frame = CGRect(x: 0, y: 0, width: 288, height: 48)
+        // 阴影
+        view.backgroundColor = .clear
+        view.layer.shadowColor = UIColor.black.cgColor
+        view.layer.shadowOffset = CGSize(width: 0, height: 2)
+        view.layer.shadowOpacity = 0.1
+        
+        // 圆角
+        let colorBg = UIView()
+        colorBg.backgroundColor = "#333333".uiColor
+        colorBg.layer.cornerRadius = 8
+        colorBg.layer.masksToBounds = true
+        colorBg.clipsToBounds = true
+        
+        view.addSubview(colorBg)
+        colorBg.snp.makeConstraints { make in
+            make.leading.trailing.top.bottom.equalTo(0)
+        }
+        
+        let image = UIImage(named: "success_icon")
+        let iconView = UIImageView(image: image)
+        view.addSubview(iconView)
+        iconView.snp.makeConstraints { make in
+            make.width.height.equalTo(24)
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(12)
+        }
+    
+        view.addSubview(viewButton)
+        view.addSubview(textLabel)
+        viewButton.snp.makeConstraints { make in
+            make.width.equalTo(65)
+            make.height.equalTo(28)
+            make.trailing.equalTo(-8)
+            make.centerY.equalToSuperview()
+        }
+    
+        textLabel.snp.makeConstraints { make in
+            make.leading.equalTo(iconView.snp.trailing).offset(8)
+            make.trailing.equalTo(viewButton.snp.leading).offset(-4)
+            make.centerY.equalToSuperview()
+        }
+
+        return view
+    }
+
+    
+    
+    public func getBottom(topY:CGFloat)->CGFloat{
+        return -(k_ScreenHeight - 48 - topY)
+    }
+    
+    public func show(atView:UIView,text:String = "Save Successfully".localized,deadline:Double = 2.0,bottom:CGFloat = -112,showViewBtn:Bool = true,clickViewHandle:(()->Void)? = nil) {
+        self.clickViewHandle = clickViewHandle
+        kExecuteOnMainThread {
+            self.textLabel.text = text
+            self.viewButton.isHidden = !showViewBtn
+            atView.addSubview(self.saveSuccessBg)
+            self.saveSuccessBg.snp.remakeConstraints { make in
+                make.width.equalTo(288)
+                make.height.equalTo(48)
+                make.centerX.equalToSuperview()
+                make.bottom.equalTo(bottom)
+            }
+        }
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + deadline) {
+            self.saveSuccessBg.removeFromSuperview()
+        }
+    }
+    
+    
+}

+ 5 - 1
TSSmalCoacopods/Classes/View/UICollectionView+Component/CollectionViewComponent.swift

@@ -31,6 +31,11 @@ open class TSCollectionView: UICollectionView {
     func clearReuseSubviews() {
         reuseSubviews.removeAll()
     }
+    
+    public var isCanGestureRecognizer:Bool = true
+    open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+        return isCanGestureRecognizer
+    }
 }
 
 open class TSCollectionViewComponent: NSObject {
@@ -183,7 +188,6 @@ extension TSCollectionViewComponent: UICollectionViewDataSource {
         guard let cellCp = cellComponent(at: indexPath) else {
             return collectionView.dequeueReusableCell(withReuseIdentifier: Self.defaultCellID, for: indexPath)
         }
-        
         let cellClass = cellCp.cellClass
         let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellClass.description(), for: indexPath)
         if let cell = cell as? TSComponentView {

+ 26 - 2
TSSmalCoacopods/Classes/View/UIStackView/TSCustomStackView.swift

@@ -83,18 +83,30 @@ open class TSCustomStackView: UIView {
     }
     
     // 动态添加子视图的方法
-    public func addSubviewToStack(_ view: UIView) {
+    public func addSubviewToStack(_ view: UIView,length:CGFloat? = nil,animate:Bool = false) {
         stackView.addArrangedSubview(view)
-        // 可以根据需要对子视图进行额外的布局设置
         view.snp.makeConstraints { make in
             if axis == .vertical {
                 make.width.equalTo(stackView)
+                if let length = length {
+                    make.height.equalTo(length)
+                }
             } else {
                 make.height.equalTo(stackView)
+                if let length = length {
+                    make.width.equalTo(length)
+                }
             }
         }
     }
     
+    //动态添加子视图的方法(添加到白板一个空板 View)
+    public func addSubviewToStackWhiteBoard(_ view: UIView,length:CGFloat? = nil) {
+        let bgView = UIView()
+        bgView.addSubview(view)
+        addSubviewToStack(bgView,length: length)
+    }
+    
     // 在指定位置插入子视图
     public func insertViewToStack(_ view: UIView, at stackIndex: Int) {
         stackView.insertArrangedSubview(view, at: stackIndex)
@@ -113,4 +125,16 @@ open class TSCustomStackView: UIView {
         stackView.removeArrangedSubview(view)
         view.removeFromSuperview()
     }
+    
+    public func addSpacing(length:CGFloat) {
+        let view = UIView()
+        addSubviewToStack(view)
+        view.snp.makeConstraints { make in
+            if axis == .vertical {
+                make.height.equalTo(length)
+            } else {
+                make.width.equalTo(length)
+            }
+        }
+    }
 }