ソースを参照

Merge branch '音频剪辑'

* 音频剪辑:
  1.9(6)打包上架
  修改1.9(3)bug
  修改自测的 bug,打包提测
  开发测试完毕
  1.修改获取音频时长的方法 2.解决 cell 复用机制的漏洞
  继续推进封装了下,下载铃声的各种复杂场景
  完善了TSAIRintoneHistoryCell setShareBand 逻辑
  音频剪辑,开发到设置铃声逻辑修改地方了
  剪辑音频,把关键代码拉过来了
  新增ffmpeg-kit-ios-full 第三方
  给 TSUserDefaultData 精简优化

# Conflicts:
#	AIRingtone.xcodeproj/project.pbxproj
100Years 1 週間 前
コミット
99f79e7471
71 ファイル変更3197 行追加1064 行削除
  1. 70 16
      AIRingtone.xcodeproj/project.pbxproj
  2. 22 0
      AIRingtone/Assets.xcassets/AIRing/edit_ring_icon.imageset/Contents.json
  3. BIN
      AIRingtone/Assets.xcassets/AIRing/edit_ring_icon.imageset/edit_ring_icon@2x.png
  4. BIN
      AIRingtone/Assets.xcassets/AIRing/edit_ring_icon.imageset/edit_ring_icon@3x.png
  5. 22 0
      AIRingtone/Assets.xcassets/AIRing/editaudio_pause.imageset/Contents.json
  6. BIN
      AIRingtone/Assets.xcassets/AIRing/editaudio_pause.imageset/editaudio_pause@2x.png
  7. BIN
      AIRingtone/Assets.xcassets/AIRing/editaudio_pause.imageset/editaudio_pause@3x.png
  8. 22 0
      AIRingtone/Assets.xcassets/AIRing/editaudio_play.imageset/Contents.json
  9. BIN
      AIRingtone/Assets.xcassets/AIRing/editaudio_play.imageset/editaudio_play@2x.png
  10. BIN
      AIRingtone/Assets.xcassets/AIRing/editaudio_play.imageset/editaudio_play@3x.png
  11. 22 0
      AIRingtone/Assets.xcassets/AIRing/trackBg_gradient.imageset/Contents.json
  12. BIN
      AIRingtone/Assets.xcassets/AIRing/trackBg_gradient.imageset/trackBg_gradient@2x.png
  13. BIN
      AIRingtone/Assets.xcassets/AIRing/trackBg_gradient.imageset/trackBg_gradient@3x.png
  14. 22 0
      AIRingtone/Assets.xcassets/Common/edit_field.imageset/Contents.json
  15. BIN
      AIRingtone/Assets.xcassets/Common/edit_field.imageset/edit_field@2x.png
  16. BIN
      AIRingtone/Assets.xcassets/Common/edit_field.imageset/edit_field@3x.png
  17. 155 211
      AIRingtone/Business/Data/TSUserDefaultData.swift
  18. 2 2
      AIRingtone/Business/TSAIPhotoVC/TSAIPhotoChildVC/TSAIPhotoChildVM.swift
  19. 1 1
      AIRingtone/Business/TSAIPhotoVC/TSGeneralPicBrowseVC/TSGeneralPhotoBrowseVC.swift
  20. 3 3
      AIRingtone/Business/TSAIPhotoVC/TSGeneralPicBrowseVC/TSGeneralPicBrowseVC.swift
  21. 1 1
      AIRingtone/Business/TSAIPhotoVC/TSGeneralPicBrowseVC/TSGeneralPosterBrowseVC.swift
  22. 2 2
      AIRingtone/Business/TSAIPhotoVC/TSGeneralPicVC/TSGeneralPicVC.swift
  23. 18 2
      AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/Model/TSRingModel.swift
  24. 31 22
      AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/TSAIRintoneVC.swift
  25. 173 42
      AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/View/TSAIRintoneHistoryCell.swift
  26. 3 3
      AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/ViewModel/TSAIRintoneVM.swift
  27. 0 4
      AIRingtone/Business/TSAIRintoneVC/TSGeneralRintoneVC/TSGeneralRintoneVC+Event.swift
  28. 24 7
      AIRingtone/Business/TSAIRintoneVC/TSGeneralRintoneVC/TSGeneralRintoneVC.swift
  29. 3 22
      AIRingtone/Business/TSAIRintoneVC/TSGenerateHistoryVC/TSGenerateHistoryVC.swift
  30. 3 3
      AIRingtone/Business/TSAIRintoneVC/TSGenerateHistoryVC/TSGenerateHistoryVM.swift
  31. 6 5
      AIRingtone/Business/TSCollectionViewVM/TSCollectionViewVM+Config.swift
  32. 1 20
      AIRingtone/Business/TSDiscoverVC/TSDiscoverListVC/TSDiscoverListVC.swift
  33. 1 1
      AIRingtone/Business/TSDiscoverVC/TSDiscoverVC/TSDiscoverVC.swift
  34. 13 30
      AIRingtone/Business/TSDiscoverVC/TSRingDownVC/TSRingDownVC.swift
  35. 5 8
      AIRingtone/Business/TSDiscoverVC/TSRingDownVC/VM/TSRingDownVM.swift
  36. 55 0
      AIRingtone/Business/TSEditAudioVideoVC/TSEditAudioSliderView.swift
  37. 825 0
      AIRingtone/Business/TSEditAudioVideoVC/TSEditAudioVC.swift
  38. 205 0
      AIRingtone/Business/TSEditAudioVideoVC/TSEditAudioVideoBaseVC.swift
  39. 1 1
      AIRingtone/Business/TSSetingVC/SetingVC/TSSetingVC.swift
  40. 12 3
      AIRingtone/Business/TSThemeVC/TSThemeBrowseVC/TSThemeBrowseVC.swift
  41. 9 3
      AIRingtone/Business/TSThemeVC/TSThemeSetVC/TSThemeSetVC.swift
  42. 1 1
      AIRingtone/Business/TSThemeVC/TSThemeVC/Model/TSThemeModel.swift
  43. 50 8
      AIRingtone/Business/VIewTool/TSButton.swift
  44. 13 0
      AIRingtone/Business/VIewTool/TSRingLoadingView.swift
  45. 39 3
      AIRingtone/Business/VIewTool/TSRingToneCellView.swift
  46. 18 11
      AIRingtone/Business/VIewTool/TSSavePhotoSuccessTool.swift
  47. 31 34
      AIRingtone/Common/NetworkManager/TSNetWork/TSNetWork+Business.swift
  48. 65 0
      AIRingtone/Common/NetworkManager/TSNetWork/TSNetworkManager.swift
  49. 2 2
      AIRingtone/Common/Purchase/TSPurchaseManager/TSPurchaseTool.swift
  50. 56 56
      AIRingtone/Common/Tool/NotUse/TSAudioPlayerFileTool.swift
  51. 0 214
      AIRingtone/Common/Tool/NotUse/TSDownloadManager.swift
  52. 214 0
      AIRingtone/Common/Tool/NotUse/TSDownloadTool.swift
  53. 3 3
      AIRingtone/Common/Tool/OperationQueue/TSGenerateBaseOperation/TSGeneratePhotoOperation.swift
  54. 3 3
      AIRingtone/Common/Tool/OperationQueue/TSGenerateBaseOperation/TSGeneratePosterOperation.swift
  55. 3 3
      AIRingtone/Common/Tool/OperationQueue/TSGenerateBaseOperation/TSGenerateRintoneOperation.swift
  56. 2 2
      AIRingtone/Common/Tool/TSAudioPlayer/TSAudioPlayer.swift
  57. 106 19
      AIRingtone/Common/Tool/TSAudioPlayer/TSBusinessAudioPlayer.swift
  58. 78 104
      AIRingtone/Common/Tool/TSBandRingTool/AudioTool.swift
  59. 44 25
      AIRingtone/Common/Tool/TSBandRingTool/TSBandRingTool.swift
  60. 55 0
      AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHAudioProcessing.swift
  61. 37 0
      AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHCroppedDelegate.swift
  62. 36 0
      AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHTrackProcessing.swift
  63. 311 0
      AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHWaveformView.swift
  64. 19 0
      AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHWaveformViewDelegate.swift
  65. 7 0
      AIRingtone/Common/Tool/TSBusinessCommon.swift
  66. 151 0
      AIRingtone/Common/Tool/TSBusinessFileManager.swift
  67. 112 0
      AIRingtone/Common/Tool/TSDownloadManager.swift
  68. 0 157
      AIRingtone/Common/Tool/TSFileManagerTool.swift
  69. 0 6
      AIRingtone/Common/Tool/TSPublicContent.swift
  70. 4 0
      Podfile
  71. 5 1
      Podfile.lock

+ 70 - 16
AIRingtone.xcodeproj/project.pbxproj

@@ -17,7 +17,6 @@
 		A80EDECB2D718CEA003CD332 /* TSNetWork+Business.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80EDE6D2D718CEA003CD332 /* TSNetWork+Business.swift */; };
 		A80EDEDB2D718CEA003CD332 /* TSPhotoPickerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80EDEBB2D718CEA003CD332 /* TSPhotoPickerManager.swift */; };
 		A80EDEE32D718CEA003CD332 /* TSNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80EDE6C2D718CEA003CD332 /* TSNetworkManager.swift */; };
-		A80EDEE92D718CEA003CD332 /* TSFileManagerTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80EDEB62D718CEA003CD332 /* TSFileManagerTool.swift */; };
 		A80EDEEA2D718CEA003CD332 /* PaddedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80EDEBF2D718CEA003CD332 /* PaddedLabel.swift */; };
 		A80EDEEB2D718CEA003CD332 /* StreamPostRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80EDE6F2D718CEA003CD332 /* StreamPostRequest.swift */; };
 		A80EDEF52D718DEA003CD332 /* TSTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80EDEF32D718DEA003CD332 /* TSTabBarController.swift */; };
@@ -58,6 +57,13 @@
 		A840A7FA2D916D9B0044B8B9 /* TSGeneratePosterOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A7F92D916D9A0044B8B9 /* TSGeneratePosterOperation.swift */; };
 		A840A7FE2D916DB50044B8B9 /* TSGeneratePhotoOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A7FD2D916DB40044B8B9 /* TSGeneratePhotoOperation.swift */; };
 		A840A8032D91945A0044B8B9 /* TSGenerateBaseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A8022D9194590044B8B9 /* TSGenerateBaseOperation.swift */; };
+		A840A8082D94044E0044B8B9 /* TSEditAudioVideoBaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A8072D9404480044B8B9 /* TSEditAudioVideoBaseVC.swift */; };
+		A840A80F2D94057B0044B8B9 /* ZHAudioProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A8092D94057B0044B8B9 /* ZHAudioProcessing.swift */; };
+		A840A8102D94057B0044B8B9 /* ZHWaveformViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80A2D94057B0044B8B9 /* ZHWaveformViewDelegate.swift */; };
+		A840A8112D94057B0044B8B9 /* ZHWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80C2D94057B0044B8B9 /* ZHWaveformView.swift */; };
+		A840A8122D94057B0044B8B9 /* ZHCroppedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80D2D94057B0044B8B9 /* ZHCroppedDelegate.swift */; };
+		A840A8132D94057B0044B8B9 /* ZHTrackProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A840A80B2D94057B0044B8B9 /* ZHTrackProcessing.swift */; };
+		A85ACE112D9A6AD200ED56A1 /* TSEditAudioVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85ACE102D9A6ACD00ED56A1 /* TSEditAudioVC.swift */; };
 		A868A89A2D75505E00F6D884 /* TSThemeBannerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A8992D75505800F6D884 /* TSThemeBannerCell.swift */; };
 		A868A89C2D75506C00F6D884 /* TSThemeContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A89B2D75506500F6D884 /* TSThemeContentCell.swift */; };
 		A868A8A22D7560B900F6D884 /* TSPageNullView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868A89D2D7560B900F6D884 /* TSPageNullView.swift */; };
@@ -120,13 +126,16 @@
 		A899D39C2D894F9800AB9C1C /* TSRingDownVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D39B2D894F9600AB9C1C /* TSRingDownVC.swift */; };
 		A899D3A32D896A7B00AB9C1C /* TSRingDownNullView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D39F2D895F3C00AB9C1C /* TSRingDownNullView.swift */; };
 		A899D3A52D89786200AB9C1C /* TSAudioAVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3A42D89785E00AB9C1C /* TSAudioAVPlayer.swift */; };
-		A899D3CB2D89A53D00AB9C1C /* TSDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3CA2D89A53C00AB9C1C /* TSDownloadManager.swift */; };
+		A899D3CB2D89A53D00AB9C1C /* TSDownloadTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3CA2D89A53C00AB9C1C /* TSDownloadTool.swift */; };
 		A899D3CF2D89AD4C00AB9C1C /* TSAudioPlayerFileTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3CE2D89AD4800AB9C1C /* TSAudioPlayerFileTool.swift */; };
 		A899D3D12D89B14B00AB9C1C /* TSDiscoverListCellVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3D02D89B14400AB9C1C /* TSDiscoverListCellVM.swift */; };
 		A899D3D42D8A6B6600AB9C1C /* TSGenerateHistoryVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3D32D8A6B6500AB9C1C /* TSGenerateHistoryVC.swift */; };
 		A899D3D62D8A6CC600AB9C1C /* TSGenerateHistoryVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3D52D8A6CC200AB9C1C /* TSGenerateHistoryVM.swift */; };
 		A899D3D82D8A76CE00AB9C1C /* TSAIRintoneHistorySectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3D72D8A76CB00AB9C1C /* TSAIRintoneHistorySectionHeaderView.swift */; };
 		A899D3DB2D8A97F100AB9C1C /* TSRingDownVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A899D3DA2D8A97EA00AB9C1C /* TSRingDownVM.swift */; };
+		A8A93A912D95206C00ABF9C6 /* TSBusinessCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A93A902D95204900ABF9C6 /* TSBusinessCommon.swift */; };
+		A8A93A932D954C1000ABF9C6 /* TSEditAudioSliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A93A922D954C0800ABF9C6 /* TSEditAudioSliderView.swift */; };
+		A8A93A952D95616600ABF9C6 /* TSBusinessFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A93A942D95616500ABF9C6 /* TSBusinessFileManager.swift */; };
 		A8C6436C2D79A8C8001068D0 /* TSAIRintoneHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C6436B2D79A8C7001068D0 /* TSAIRintoneHistoryCell.swift */; };
 		A8CC55822D797720002E0CAA /* TSGeneralPicBrowseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8CC55812D79771F002E0CAA /* TSGeneralPicBrowseVC.swift */; };
 		A8CC55862D798E2D002E0CAA /* TSTextGeneralRintoneVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8CC55852D798E2D002E0CAA /* TSTextGeneralRintoneVC.swift */; };
@@ -135,7 +144,7 @@
 		A8CC55902D799F9F002E0CAA /* TSAIRintoneVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8CC558F2D799F9C002E0CAA /* TSAIRintoneVM.swift */; };
 		A8D776F82D8D3448007EAB35 /* TSBaseOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D776F72D8D343F007EAB35 /* TSBaseOperationQueue.swift */; };
 		A8D776FA2D8D345C007EAB35 /* TSBaseOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D776F92D8D345B007EAB35 /* TSBaseOperation.swift */; };
-		A8D776FE2D8D3E5E007EAB35 /* TSPublicContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D776FD2D8D3E4E007EAB35 /* TSPublicContent.swift */; };
+		A8D776FE2D8D3E5E007EAB35 /* TSDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D776FD2D8D3E4E007EAB35 /* TSDownloadManager.swift */; };
 		A8DEC2702D7C395C002EB948 /* TSThemeCopyrightVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8DEC26F2D7C395B002EB948 /* TSThemeCopyrightVC.swift */; };
 		F5FF0EC10B0056B65FDB9C78 /* Pods_AIRingtone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39C823AB7F3D49E8B2356E07 /* Pods_AIRingtone.framework */; };
 /* End PBXBuildFile section */
@@ -156,7 +165,6 @@
 		A80EDE6E2D718CEA003CD332 /* TSNetworkManager+Loading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSNetworkManager+Loading.swift"; sourceTree = "<group>"; };
 		A80EDE6F2D718CEA003CD332 /* StreamPostRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPostRequest.swift; sourceTree = "<group>"; };
 		A80EDEB32D718CEA003CD332 /* TSRandomTextPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRandomTextPicker.swift; sourceTree = "<group>"; };
-		A80EDEB62D718CEA003CD332 /* TSFileManagerTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSFileManagerTool.swift; sourceTree = "<group>"; };
 		A80EDEBB2D718CEA003CD332 /* TSPhotoPickerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSPhotoPickerManager.swift; sourceTree = "<group>"; };
 		A80EDEBF2D718CEA003CD332 /* PaddedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddedLabel.swift; sourceTree = "<group>"; };
 		A80EDEF32D718DEA003CD332 /* TSTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSTabBarController.swift; sourceTree = "<group>"; };
@@ -197,6 +205,13 @@
 		A840A7F92D916D9A0044B8B9 /* TSGeneratePosterOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGeneratePosterOperation.swift; sourceTree = "<group>"; };
 		A840A7FD2D916DB40044B8B9 /* TSGeneratePhotoOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGeneratePhotoOperation.swift; sourceTree = "<group>"; };
 		A840A8022D9194590044B8B9 /* TSGenerateBaseOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGenerateBaseOperation.swift; sourceTree = "<group>"; };
+		A840A8072D9404480044B8B9 /* TSEditAudioVideoBaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSEditAudioVideoBaseVC.swift; sourceTree = "<group>"; };
+		A840A8092D94057B0044B8B9 /* ZHAudioProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHAudioProcessing.swift; sourceTree = "<group>"; };
+		A840A80A2D94057B0044B8B9 /* ZHWaveformViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHWaveformViewDelegate.swift; sourceTree = "<group>"; };
+		A840A80B2D94057B0044B8B9 /* ZHTrackProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHTrackProcessing.swift; sourceTree = "<group>"; };
+		A840A80C2D94057B0044B8B9 /* ZHWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHWaveformView.swift; sourceTree = "<group>"; };
+		A840A80D2D94057B0044B8B9 /* ZHCroppedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZHCroppedDelegate.swift; sourceTree = "<group>"; };
+		A85ACE102D9A6ACD00ED56A1 /* TSEditAudioVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSEditAudioVC.swift; sourceTree = "<group>"; };
 		A868A8992D75505800F6D884 /* TSThemeBannerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThemeBannerCell.swift; sourceTree = "<group>"; };
 		A868A89B2D75506500F6D884 /* TSThemeContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThemeContentCell.swift; sourceTree = "<group>"; };
 		A868A89D2D7560B900F6D884 /* TSPageNullView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSPageNullView.swift; sourceTree = "<group>"; };
@@ -261,13 +276,16 @@
 		A899D39B2D894F9600AB9C1C /* TSRingDownVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRingDownVC.swift; sourceTree = "<group>"; };
 		A899D39F2D895F3C00AB9C1C /* TSRingDownNullView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRingDownNullView.swift; sourceTree = "<group>"; };
 		A899D3A42D89785E00AB9C1C /* TSAudioAVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAudioAVPlayer.swift; sourceTree = "<group>"; };
-		A899D3CA2D89A53C00AB9C1C /* TSDownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSDownloadManager.swift; sourceTree = "<group>"; };
+		A899D3CA2D89A53C00AB9C1C /* TSDownloadTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSDownloadTool.swift; sourceTree = "<group>"; };
 		A899D3CE2D89AD4800AB9C1C /* TSAudioPlayerFileTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAudioPlayerFileTool.swift; sourceTree = "<group>"; };
 		A899D3D02D89B14400AB9C1C /* TSDiscoverListCellVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSDiscoverListCellVM.swift; sourceTree = "<group>"; };
 		A899D3D32D8A6B6500AB9C1C /* TSGenerateHistoryVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGenerateHistoryVC.swift; sourceTree = "<group>"; };
 		A899D3D52D8A6CC200AB9C1C /* TSGenerateHistoryVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGenerateHistoryVM.swift; sourceTree = "<group>"; };
 		A899D3D72D8A76CB00AB9C1C /* TSAIRintoneHistorySectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAIRintoneHistorySectionHeaderView.swift; sourceTree = "<group>"; };
 		A899D3DA2D8A97EA00AB9C1C /* TSRingDownVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRingDownVM.swift; sourceTree = "<group>"; };
+		A8A93A902D95204900ABF9C6 /* TSBusinessCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSBusinessCommon.swift; sourceTree = "<group>"; };
+		A8A93A922D954C0800ABF9C6 /* TSEditAudioSliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSEditAudioSliderView.swift; sourceTree = "<group>"; };
+		A8A93A942D95616500ABF9C6 /* TSBusinessFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSBusinessFileManager.swift; sourceTree = "<group>"; };
 		A8C6436B2D79A8C7001068D0 /* TSAIRintoneHistoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAIRintoneHistoryCell.swift; sourceTree = "<group>"; };
 		A8CC55812D79771F002E0CAA /* TSGeneralPicBrowseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSGeneralPicBrowseVC.swift; sourceTree = "<group>"; };
 		A8CC55852D798E2D002E0CAA /* TSTextGeneralRintoneVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSTextGeneralRintoneVC.swift; sourceTree = "<group>"; };
@@ -276,7 +294,7 @@
 		A8CC558F2D799F9C002E0CAA /* TSAIRintoneVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAIRintoneVM.swift; sourceTree = "<group>"; };
 		A8D776F72D8D343F007EAB35 /* TSBaseOperationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSBaseOperationQueue.swift; sourceTree = "<group>"; };
 		A8D776F92D8D345B007EAB35 /* TSBaseOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSBaseOperation.swift; sourceTree = "<group>"; };
-		A8D776FD2D8D3E4E007EAB35 /* TSPublicContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSPublicContent.swift; sourceTree = "<group>"; };
+		A8D776FD2D8D3E4E007EAB35 /* TSDownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSDownloadManager.swift; sourceTree = "<group>"; };
 		A8DEC26F2D7C395B002EB948 /* TSThemeCopyrightVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThemeCopyrightVC.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -368,6 +386,7 @@
 		A80EDE632D718B19003CD332 /* Business */ = {
 			isa = PBXGroup;
 			children = (
+				A840A8062D9404060044B8B9 /* TSEditAudioVideoVC */,
 				A83F871B2D79408300D29B1B /* Data */,
 				A899D3822D88303300AB9C1C /* TSDiscoverVC */,
 				A899D34C2D82C61C00AB9C1C /* TSPurchaseMembershipVC */,
@@ -406,14 +425,15 @@
 		A80EDEBA2D718CEA003CD332 /* Tool */ = {
 			isa = PBXGroup;
 			children = (
-				A8D776FD2D8D3E4E007EAB35 /* TSPublicContent.swift */,
+				A8A93A942D95616500ABF9C6 /* TSBusinessFileManager.swift */,
+				A8A93A902D95204900ABF9C6 /* TSBusinessCommon.swift */,
+				A8D776FD2D8D3E4E007EAB35 /* TSDownloadManager.swift */,
 				A8D776F62D8D342D007EAB35 /* OperationQueue */,
 				A840A7EE2D8D82100044B8B9 /* NotUse */,
 				A868A8DC2D76F90E00F6D884 /* TSBandRingTool */,
 				A868A8BC2D75C68300F6D884 /* TSAudioPlayer */,
 				A80EDEB32D718CEA003CD332 /* TSRandomTextPicker.swift */,
 				A868A8F02D77081B00F6D884 /* TSContactsTool.swift */,
-				A80EDEB62D718CEA003CD332 /* TSFileManagerTool.swift */,
 				A868A8EE2D77040F00F6D884 /* TSLoadingAnimation.swift */,
 			);
 			path = Tool;
@@ -638,7 +658,7 @@
 		A840A7EE2D8D82100044B8B9 /* NotUse */ = {
 			isa = PBXGroup;
 			children = (
-				A899D3CA2D89A53C00AB9C1C /* TSDownloadManager.swift */,
+				A899D3CA2D89A53C00AB9C1C /* TSDownloadTool.swift */,
 				A899D3A42D89785E00AB9C1C /* TSAudioAVPlayer.swift */,
 				A899D3CE2D89AD4800AB9C1C /* TSAudioPlayerFileTool.swift */,
 			);
@@ -656,6 +676,28 @@
 			path = TSGenerateBaseOperation;
 			sourceTree = "<group>";
 		};
+		A840A8062D9404060044B8B9 /* TSEditAudioVideoVC */ = {
+			isa = PBXGroup;
+			children = (
+				A85ACE102D9A6ACD00ED56A1 /* TSEditAudioVC.swift */,
+				A8A93A922D954C0800ABF9C6 /* TSEditAudioSliderView.swift */,
+				A840A8072D9404480044B8B9 /* TSEditAudioVideoBaseVC.swift */,
+			);
+			path = TSEditAudioVideoVC;
+			sourceTree = "<group>";
+		};
+		A840A80E2D94057B0044B8B9 /* ZHWaveform */ = {
+			isa = PBXGroup;
+			children = (
+				A840A8092D94057B0044B8B9 /* ZHAudioProcessing.swift */,
+				A840A80A2D94057B0044B8B9 /* ZHWaveformViewDelegate.swift */,
+				A840A80B2D94057B0044B8B9 /* ZHTrackProcessing.swift */,
+				A840A80C2D94057B0044B8B9 /* ZHWaveformView.swift */,
+				A840A80D2D94057B0044B8B9 /* ZHCroppedDelegate.swift */,
+			);
+			path = ZHWaveform;
+			sourceTree = "<group>";
+		};
 		A868A8982D75505100F6D884 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -766,6 +808,7 @@
 				A868A8EC2D76FE5D00F6D884 /* tutorial-ring.mp4 */,
 				A868A8DA2D76F00800F6D884 /* TSBandRingTool.swift */,
 				A868A8EA2D76FD9800F6D884 /* placeholder.band */,
+				A840A80E2D94057B0044B8B9 /* ZHWaveform */,
 			);
 			path = TSBandRingTool;
 			sourceTree = "<group>";
@@ -1190,6 +1233,7 @@
 				A868A9002D77E55800F6D884 /* TSPromptStyleView.swift in Sources */,
 				A83F87202D794FF000D29B1B /* TSAIPhotoImageCell.swift in Sources */,
 				A899D3932D8924A300AB9C1C /* TSDiscoverListVC.swift in Sources */,
+				A840A8082D94044E0044B8B9 /* TSEditAudioVideoBaseVC.swift in Sources */,
 				A868A8DB2D76F00C00F6D884 /* TSBandRingTool.swift in Sources */,
 				A840A7F42D8EA0340044B8B9 /* TSGeneralPhotoBrowseVC.swift in Sources */,
 				A8D776FA2D8D345C007EAB35 /* TSBaseOperation.swift in Sources */,
@@ -1202,7 +1246,7 @@
 				A80EDE652D718CAF003CD332 /* GlobalImports.swift in Sources */,
 				A840A7ED2D8D81DB0044B8B9 /* TSGenerateRintoneOperation.swift in Sources */,
 				A868A9092D7828EC00F6D884 /* TSTGPPhotoStyleView.swift in Sources */,
-				A8D776FE2D8D3E5E007EAB35 /* TSPublicContent.swift in Sources */,
+				A8D776FE2D8D3E5E007EAB35 /* TSDownloadManager.swift in Sources */,
 				A8DEC2702D7C395C002EB948 /* TSThemeCopyrightVC.swift in Sources */,
 				A8D776F82D8D3448007EAB35 /* TSBaseOperationQueue.swift in Sources */,
 				A80EDF112D718ED1003CD332 /* TSAIRintoneVC.swift in Sources */,
@@ -1214,6 +1258,7 @@
 				A80EDEC62D718CEA003CD332 /* TSRandomTextPicker.swift in Sources */,
 				A899D3D12D89B14B00AB9C1C /* TSDiscoverListCellVM.swift in Sources */,
 				A80EDEC82D718CEA003CD332 /* TSNetworkManager+Loading.swift in Sources */,
+				A8A93A952D95616600ABF9C6 /* TSBusinessFileManager.swift in Sources */,
 				A899D34D2D82C61C00AB9C1C /* TSPurchaseVC.swift in Sources */,
 				A868A9182D78555200F6D884 /* TSGenneralPicVM.swift in Sources */,
 				A80EDECB2D718CEA003CD332 /* TSNetWork+Business.swift in Sources */,
@@ -1250,21 +1295,28 @@
 				A868A8BA2D75C22300F6D884 /* TSThemeBrowseVM.swift in Sources */,
 				A868A89C2D75506C00F6D884 /* TSThemeContentCell.swift in Sources */,
 				A868A91E2D785CEA00F6D884 /* TSGeneralPicVC+Event.swift in Sources */,
-				A80EDEE92D718CEA003CD332 /* TSFileManagerTool.swift in Sources */,
 				A868A8A22D7560B900F6D884 /* TSPageNullView.swift in Sources */,
 				A868A8CE2D76AAC600F6D884 /* TSThemeSetRingToneView.swift in Sources */,
 				A868A90E2D7846D600F6D884 /* TSSavePhotoSuccessTool.swift in Sources */,
+				A85ACE112D9A6AD200ED56A1 /* TSEditAudioVC.swift in Sources */,
 				A8C6436C2D79A8C8001068D0 /* TSAIRintoneHistoryCell.swift in Sources */,
 				A868A8A32D7560B900F6D884 /* TSViewTool.swift in Sources */,
 				A83F871D2D79409B00D29B1B /* TSUserDefaultData.swift in Sources */,
 				A868A9112D784CFB00F6D884 /* TSGeneralPicVC.swift in Sources */,
 				A8272EBB2D7AFD0F00F1C814 /* TSBusinessAudioPlayer.swift in Sources */,
+				A8A93A912D95206C00ABF9C6 /* TSBusinessCommon.swift in Sources */,
 				A868A8A42D7560B900F6D884 /* TSCommonloadingView.swift in Sources */,
 				A868A89A2D75505E00F6D884 /* TSThemeBannerCell.swift in Sources */,
+				A840A80F2D94057B0044B8B9 /* ZHAudioProcessing.swift in Sources */,
+				A840A8102D94057B0044B8B9 /* ZHWaveformViewDelegate.swift in Sources */,
+				A840A8112D94057B0044B8B9 /* ZHWaveformView.swift in Sources */,
+				A840A8122D94057B0044B8B9 /* ZHCroppedDelegate.swift in Sources */,
+				A840A8132D94057B0044B8B9 /* ZHTrackProcessing.swift in Sources */,
 				A80EDF182D7193EE003CD332 /* TSTutorialsVC.swift in Sources */,
 				A899D3A52D89786200AB9C1C /* TSAudioAVPlayer.swift in Sources */,
 				A80EDEEA2D718CEA003CD332 /* PaddedLabel.swift in Sources */,
 				A8CC55862D798E2D002E0CAA /* TSTextGeneralRintoneVC.swift in Sources */,
+				A8A93A932D954C1000ABF9C6 /* TSEditAudioSliderView.swift in Sources */,
 				A80EDEEB2D718CEA003CD332 /* StreamPostRequest.swift in Sources */,
 				A899D3D62D8A6CC600AB9C1C /* TSGenerateHistoryVM.swift in Sources */,
 				A80EDF2A2D71C215003CD332 /* TSThemeVM.swift in Sources */,
@@ -1283,7 +1335,7 @@
 				A868A8F82D77E2BC00F6D884 /* TSTextGeneralPicVC.swift in Sources */,
 				A840A7FE2D916DB50044B8B9 /* TSGeneratePhotoOperation.swift in Sources */,
 				A80EDF022D718DF1003CD332 /* TSSettingListView.swift in Sources */,
-				A899D3CB2D89A53D00AB9C1C /* TSDownloadManager.swift in Sources */,
+				A899D3CB2D89A53D00AB9C1C /* TSDownloadTool.swift in Sources */,
 				A8CC55902D799F9F002E0CAA /* TSAIRintoneVM.swift in Sources */,
 				A80EDF032D718DF1003CD332 /* ShareActivityItemProvider.swift in Sources */,
 				A80EDF042D718DF1003CD332 /* TSSetingModel.swift in Sources */,
@@ -1323,11 +1375,12 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = 6;
 				DEVELOPMENT_TEAM = 65UD255J84;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = AIRingtone/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = Ringtones;
+				INFOPLIST_KEY_NSCameraUsageDescription = "Allows us to access camera permissions to handle audio clips.";
 				INFOPLIST_KEY_NSContactsUsageDescription = "Allow \"Contacts\" permission to set contact photo";
 				INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Allow us to access Photos in order to save wallpapers to your device.";
 				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -1342,7 +1395,7 @@
 					"$(inherited)",
 					"$(PROJECT_DIR)/AIRingtone/Common/Tool/TSBandRingTool/libmp3",
 				);
-				MARKETING_VERSION = 1.8;
+				MARKETING_VERSION = 1.9;
 				PRODUCT_BUNDLE_IDENTIFIER = ai.ringtones.com;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1365,11 +1418,12 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 1;
+				CURRENT_PROJECT_VERSION = 6;
 				DEVELOPMENT_TEAM = 65UD255J84;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = AIRingtone/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = Ringtones;
+				INFOPLIST_KEY_NSCameraUsageDescription = "Allows us to access camera permissions to handle audio clips.";
 				INFOPLIST_KEY_NSContactsUsageDescription = "Allow \"Contacts\" permission to set contact photo";
 				INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Allow us to access Photos in order to save wallpapers to your device.";
 				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -1384,7 +1438,7 @@
 					"$(inherited)",
 					"$(PROJECT_DIR)/AIRingtone/Common/Tool/TSBandRingTool/libmp3",
 				);
-				MARKETING_VERSION = 1.8;
+				MARKETING_VERSION = 1.9;
 				PRODUCT_BUNDLE_IDENTIFIER = ai.ringtones.com;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";

+ 22 - 0
AIRingtone/Assets.xcassets/AIRing/edit_ring_icon.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "edit_ring_icon@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "edit_ring_icon@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
AIRingtone/Assets.xcassets/AIRing/edit_ring_icon.imageset/edit_ring_icon@2x.png


BIN
AIRingtone/Assets.xcassets/AIRing/edit_ring_icon.imageset/edit_ring_icon@3x.png


+ 22 - 0
AIRingtone/Assets.xcassets/AIRing/editaudio_pause.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "editaudio_pause@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "editaudio_pause@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
AIRingtone/Assets.xcassets/AIRing/editaudio_pause.imageset/editaudio_pause@2x.png


BIN
AIRingtone/Assets.xcassets/AIRing/editaudio_pause.imageset/editaudio_pause@3x.png


+ 22 - 0
AIRingtone/Assets.xcassets/AIRing/editaudio_play.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "editaudio_play@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "editaudio_play@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
AIRingtone/Assets.xcassets/AIRing/editaudio_play.imageset/editaudio_play@2x.png


BIN
AIRingtone/Assets.xcassets/AIRing/editaudio_play.imageset/editaudio_play@3x.png


+ 22 - 0
AIRingtone/Assets.xcassets/AIRing/trackBg_gradient.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "trackBg_gradient@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "trackBg_gradient@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
AIRingtone/Assets.xcassets/AIRing/trackBg_gradient.imageset/trackBg_gradient@2x.png


BIN
AIRingtone/Assets.xcassets/AIRing/trackBg_gradient.imageset/trackBg_gradient@3x.png


+ 22 - 0
AIRingtone/Assets.xcassets/Common/edit_field.imageset/Contents.json

@@ -0,0 +1,22 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "edit_field@2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "edit_field@3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
AIRingtone/Assets.xcassets/Common/edit_field.imageset/edit_field@2x.png


BIN
AIRingtone/Assets.xcassets/Common/edit_field.imageset/edit_field@3x.png


+ 155 - 211
AIRingtone/Business/Data/TSUserDefaultData.swift

@@ -7,7 +7,7 @@
 import ObjectMapper
 
 func kHandleTSHistory(){
-    for model in TSAIRintoneHistory.listModelArray {
+    for model in TSAIRintoneHistory.shared.listModels {
         if model.modelType != .example {
             switch model.actionStatus {
             case .pending,.running:
@@ -17,7 +17,7 @@ func kHandleTSHistory(){
         }
     }
     
-    for model in TSPosterHistory.listModelArray {
+    for model in TSPosterHistory.shared.listModels {
         if model.modelType != .example {
             switch model.actionStatus {
             case .pending,.running:
@@ -27,7 +27,7 @@ func kHandleTSHistory(){
         }
     }
     
-    for model in TSPhotoHistory.listModelArray {
+    for model in TSPhotoHistory.shared.listModels {
         if model.modelType != .example {
             switch model.actionStatus {
             case .pending,.running:
@@ -36,232 +36,197 @@ func kHandleTSHistory(){
             }
         }
     }
-    
-
 }
-//海报历史记录
-class TSPosterHistory{
-    @UserDefault(key: "kPosterTextPicHistoryListString", defaultValue: "")
-    static private var historyString: String
-    static var listModelArray: [TSActionInfoModel] = {
-        
-        if UserDefaults.standard.string(forKey: "insertPosterExampleData") == nil {
-            insertExampleData()
-            UserDefaults.standard.set("1", forKey: "insertPosterExampleData")
-            UserDefaults.standard.synchronize()
-        }
+
+// MARK: - 基础历史记录类
+class TSBaseHistoryManager<ModelType: TSBaseModel> {
+    // 子类必须重写的属性
+    var historyKey: String { fatalError("必须重写 historyKey") }
+    var exampleDataKey: String { fatalError("必须重写 exampleDataKey") }
+    var exampleModels: [ModelType] { fatalError("必须重写 exampleModels") }
+
+    func findModelID(modelID: Int)->Int?{
+        fatalError("必须重写 findModelID")
+    }
+
+    func saveModelAfterProcess(){
         
-        if let listModelArray = Mapper<TSActionInfoModel>().mapArray(JSONString: historyString){
-            return listModelArray
-        }
-        return []
-    }()
-    
-    static func saveModel(model:TSActionInfoModel){
-        self.listModelArray.insert(model, at: 0)
-        self.saveHistoryString()
     }
     
-    static func removeModel(model:TSActionInfoModel){
-        self.listModelArray.removeAll { $0 === model }
-        self.saveHistoryString()
+    // 存储属性
+    private var _historyString: String {
+        get { UserDefaults.standard.string(forKey: historyKey) ?? "" }
+        set { UserDefaults.standard.set(newValue, forKey: historyKey) }
     }
     
-    
-    static func replaceModel(oldID: Int, newModel: TSActionInfoModel) {
-        if let index = self.listModelArray.firstIndex(where: {$0.id == oldID}) {
-            self.listModelArray[index] = newModel
-            dePrint("TSPosterHistory.listModelArray Model replaced at index \(index)")
-        } else {
-            self.listModelArray.insert(newModel, at: 0)
-            dePrint("TSPosterHistory.listModel ArrayModel not found")
+    private var _listModels: [ModelType]?
+    var listModels: [ModelType] {
+        get {
+            if _listModels == nil { loadModels() }
+            return _listModels ?? []
+        }
+        set {
+            _listModels = newValue
         }
-        dePrint("TSPosterHistory.listModelArray.count=\(TSAIRintoneHistory.listModelArray.count)")
-        self.saveHistoryString()
     }
     
-    static func removeIndex(index:Int){
-        self.listModelArray.remove(at: index)
-        self.saveHistoryString()
+    // MARK: - 公共方法
+    func saveModel(model: ModelType, at index: Int = 0) {
+        listModels.insert(model, at: index)
+        saveHistory()
+        saveModelAfterProcess()
     }
     
-    static func saveHistoryString(){
-        if let jsonString = listModelArray.toJSONString() {
-            historyString = jsonString
-        }
+    func removeModel(model:ModelType) {
+        self.listModels.removeAll { $0 === model }
+        saveHistory()
     }
     
-    private static func insertExampleData(){
-        let array = [
-            self.createExampleModel(imageName: "poster_example_0"),
-            self.createExampleModel(imageName: "poster_example_1"),
-            self.createExampleModel(imageName: "poster_example_2")
-        ]
-        if let jsonString = array.toJSONString() {
-            self.historyString = jsonString
-        }
+    func removeModel(index: Int) {
+        guard index >= 0 && index < listModels.count else { return }
+        listModels.remove(at: index)
+        saveHistory()
     }
     
-    private static func createExampleModel(imageName:String)->TSActionInfoModel{
-        let model = TSActionInfoModel()
-        model.modelType = .example
-        model.request.prompt = "Example"
-        model.request.promptSort = "Example"
-        model.request.width = kTextPicW
-        model.request.height = kTextPicH
-        model.response.resultUrl = imageName
-        return model
-    }
-}
-
-//头像历史记录
-class TSPhotoHistory{
-    @UserDefault(key: "kPhotoTextPicHistoryListString", defaultValue: "")
-    static private var historyString: String
-    static var listModelArray: [TSActionInfoModel] = {
-        if UserDefaults.standard.string(forKey: "insertPhotoExampleData") == nil {
-            insertExampleData()
-            UserDefaults.standard.set("1", forKey: "insertPhotoExampleData")
-            UserDefaults.standard.synchronize()
-        }
-        if let listModelArray = Mapper<TSActionInfoModel>().mapArray(JSONString: historyString){
-            return listModelArray
+    func replaceModel(oldID: Int, newModel: ModelType){
+        if let index = findModelID(modelID: oldID) {
+            listModels[index] = newModel
+            dePrint("\(Self.self).listModels Model replaced at index \(index)")
+        } else {
+            listModels.insert(newModel, at: 0)
+            dePrint("\(Self.self).listModels Model not found")
         }
-        return []
-    }()
+        dePrint("\(Self.self).listModels.count=\(listModels.count)")
+        saveHistory()
+    }
     
-    static func saveModel(model:TSActionInfoModel){
-        self.listModelArray.insert(model, at: 0)
-        self.saveHistoryString()
+    func replaceAndSaveModel(saveModel:ModelType,compareBlock:(ModelType,ModelType)->Bool){
+        if let index = listModels.firstIndex(where: { model in
+            compareBlock(saveModel,model)
+        }){
+            dePrint("\(Self.self).listModels Model replaced at index \(index)")
+           listModels[index] = saveModel
+            saveHistory()
+        }else{
+            self.saveModel(model: saveModel)
+        }
     }
     
-    static func removeModel(model:TSActionInfoModel){
-        self.listModelArray.removeAll { $0 === model }
-        self.saveHistoryString()
+    func dePrintAllModel() {
+        dePrint("=======================结果查询开始======================")
+        dePrint("\(Self.self).listModels.count=\(listModels.count)")
+        for model in listModels {
+            dePrint(model.toJSON())
+        }
+        dePrint("=======================结果查询结束======================")
     }
     
-    static func removeIndex(index:Int){
-        self.listModelArray.remove(at: index)
-        self.saveHistoryString()
+    // MARK: - 私有方法
+    private func saveHistory() {
+        if let jsonString = listModels.toJSONString() {
+            _historyString = jsonString
+        }
     }
     
-    static func replaceModel(oldID: Int, newModel: TSActionInfoModel) {
-        if let index = self.listModelArray.firstIndex(where: {$0.id == oldID}) {
-            self.listModelArray[index] = newModel
-            dePrint("TSPhotoHistory.listModelArray Model replaced at index \(index)")
+    private func loadModels() {
+        if exampleModels.count > 0 {
+            // 第一次运行时插入示例数据
+            if UserDefaults.standard.string(forKey: exampleDataKey) == nil {
+                insertExampleData()
+                UserDefaults.standard.set("1", forKey: exampleDataKey)
+            }
+        }
+
+        // 从历史记录加载模型
+        if let models = Mapper<ModelType>().mapArray(JSONString: _historyString) {
+            _listModels = models
         } else {
-            self.listModelArray.insert(newModel, at: 0)
-            dePrint("TSPhotoHistory.listModel ArrayModel not found")
+            _listModels = []
         }
-        dePrint("TSPhotoHistory.listModelArray.count=\(TSAIRintoneHistory.listModelArray.count)")
-        self.saveHistoryString()
     }
     
-    static func saveHistoryString(){
-        if let jsonString = listModelArray.toJSONString() {
-            self.historyString = jsonString
+    private func insertExampleData() {
+        if let jsonString = exampleModels.toJSONString() {
+            _historyString = jsonString
         }
     }
+}
+
+// MARK: - 海报历史记录
+final class TSPosterHistory: TSBaseHistoryManager<TSActionInfoModel> {
+    static let shared = TSPosterHistory()
+    override var historyKey: String { "kPosterTextPicHistoryListString" }
+    override var exampleDataKey: String { "insertPosterExampleData" }
+    
+    override func findModelID(modelID: Int) -> Int? {
+        return listModels.firstIndex(where: {$0.id == modelID})
+    }
     
-    private static func insertExampleData(){
-        let array = [
-            self.createExampleModel(imageName: "photo_example_0"),
-            self.createExampleModel(imageName: "photo_example_1"),
-            self.createExampleModel(imageName: "photo_example_2")
+    override var exampleModels: [TSActionInfoModel] {
+        [
+            createExampleModel(imageName: "poster_example_0"),
+            createExampleModel(imageName: "poster_example_1"),
+            createExampleModel(imageName: "poster_example_2")
         ]
-        if let jsonString = array.toJSONString() {
-            self.historyString = jsonString
-        }
     }
     
-    private static func createExampleModel(imageName:String)->TSActionInfoModel{
+    private func createExampleModel(imageName: String) -> TSActionInfoModel {
         let model = TSActionInfoModel()
         model.modelType = .example
         model.request.prompt = "Example"
         model.request.promptSort = "Example"
         model.request.width = kTextPicW
-        model.request.height = kTextPicW
+        model.request.height = kTextPicH
         model.response.resultUrl = imageName
         return model
     }
 }
-
-//AI铃声历史记录
-class TSAIRintoneHistory{
-    @UserDefault(key: "kRintoneTextMusicHistoryListString", defaultValue: "")
-    static private var historyString: String
-    static var listModelArray: [TSActionInfoModel] = {
-        
-        if UserDefaults.standard.string(forKey: "insertRintoneExampleData") == nil {
-            insertExampleData()
-            UserDefaults.standard.set("1", forKey: "insertRintoneExampleData")
-            UserDefaults.standard.synchronize()
-        }
-        
-        if let listModelArray = Mapper<TSActionInfoModel>().mapArray(JSONString: historyString){
-            return listModelArray
-        }
-        return []
-    }()
-    
-    static func saveModel(model:TSActionInfoModel){
-        //若存在,则不更新替换
-        if let index = self.listModelArray.firstIndex(where: { $0.id == model.id }) {
-            self.listModelArray[index] = model
-        }else{
-            self.listModelArray.insert(model, at: 0)
-        }
-        self.saveHistoryString()
+// MARK: - 头像历史记录
+final class TSPhotoHistory: TSBaseHistoryManager<TSActionInfoModel> {
+    static let shared = TSPhotoHistory()
+    override var historyKey: String { "kPhotoTextPicHistoryListString" }
+    override var exampleDataKey: String { "insertPhotoExampleData" }
+    
+    override func findModelID(modelID: Int) -> Int? {
+        return listModels.firstIndex(where: {$0.id == modelID})
     }
     
-    static func removeModel(model:TSActionInfoModel){
-        self.listModelArray.removeAll { $0.id == model.id }
-        self.saveHistoryString()
-    }
-    
-    static func replaceModel(oldID: Int, newModel: TSActionInfoModel) {
-        if let index = self.listModelArray.firstIndex(where: {$0.id == oldID}) {
-            self.listModelArray[index] = newModel
-            dePrint("TSAIRintoneHistory.listModelArray Model replaced at index \(index)")
-        } else {
-            self.listModelArray.insert(newModel, at: 0)
-            dePrint("TSAIRintoneHistory.listModel ArrayModel not found")
-        }
-        dePrint("TSAIRintoneHistory.listModelArray.count=\(TSAIRintoneHistory.listModelArray.count)")
-        self.saveHistoryString()
+    override var exampleModels: [TSActionInfoModel] {
+        [
+            createExampleModel(imageName: "photo_example_0"),
+            createExampleModel(imageName: "photo_example_1"),
+            createExampleModel(imageName: "photo_example_2")
+        ]
     }
     
-    static func removeIndex(index:Int){
-        self.listModelArray.remove(at: index)
-        self.saveHistoryString()
+    private func createExampleModel(imageName: String) -> TSActionInfoModel {
+        let model = TSActionInfoModel()
+        model.modelType = .example
+        model.request.prompt = "Example"
+        model.request.promptSort = "Example"
+        model.request.width = kTextPicW
+        model.request.height = kTextPicW
+        model.response.resultUrl = imageName
+        return model
     }
-    
-    static func saveHistoryString(){
-        if let jsonString = listModelArray.toJSONString() {
-            self.historyString = jsonString
-        }
+}
+// MARK: - AI铃声历史记录
+final class TSAIRintoneHistory: TSBaseHistoryManager<TSActionInfoModel> {
+    static let shared = TSAIRintoneHistory()
+    override var historyKey: String { "kRintoneTextMusicHistoryListString" }
+    override var exampleDataKey: String { "insertRintoneExampleData" }
+    
+    override func findModelID(modelID: Int) -> Int? {
+        return listModels.firstIndex(where: {$0.id == modelID})
     }
     
-    
-    static func dePrintAllModel(){
-        dePrint("=======================结果查询开始======================")
-        dePrint("TSAIRintoneHistory.listModelArray.count=\(TSAIRintoneHistory.listModelArray.count)")
-        for model in TSAIRintoneHistory.listModelArray {
-            dePrint(model.toJSON())
-        }
-        dePrint("=======================结果查询结束======================")
-    }
-
-    private static func insertExampleData(){
-        let array = [
-            self.createExampleModel(),
+    override var exampleModels: [TSActionInfoModel] {
+        [
+            createExampleModel()
         ]
-        if let jsonString = array.toJSONString() {
-            historyString = jsonString
-        }
     }
     
-    private static func createExampleModel()->TSActionInfoModel{
+    private  func createExampleModel()->TSActionInfoModel{
         let model = TSActionInfoModel()
         model.modelType = .example
         model.request.duration = 30
@@ -273,45 +238,24 @@ class TSAIRintoneHistory{
         return model
     }
 }
-
-
-//我的下载铃声历史记录
-class TSMineRintoneHistory{
-    @UserDefault(key: "kMineRintoneHistoryListString", defaultValue: "")
-    static private var historyString: String
-    static var listModelArray: [TSRingModel] = {
-
-        if let listModelArray = Mapper<TSRingModel>().mapArray(JSONString: historyString){
-            return listModelArray
-        }
-        return []
-    }()
+// MARK: - 我的下载铃声历史记录
+final class TSMineRintoneHistory: TSBaseHistoryManager<TSRingModel> {
+    static let shared = TSMineRintoneHistory()
+    override var historyKey: String { "kMineRintoneHistoryListString" }
+    override var exampleDataKey: String { "insertMineRintoneExampleData" }
     
-    static func saveModel(model:TSRingModel){
-        if let _ = self.listModelArray.first(where: { $0.audioUrl == model.audioUrl }) {
-            return
-        }
-        self.listModelArray.insert(model, at: 0)
-        self.saveHistoryString()
-        isHaveNew = true
-    }
+    var isHaveNew:Bool = false
     
-    static func removeModel(model:TSRingModel){
-        listModelArray.removeAll { $0 === model }
-        self.saveHistoryString()
+    override func findModelID(modelID: Int) -> Int? {
+        return nil
     }
     
-    static func removeIndex(index:Int){
-        listModelArray.remove(at: index)
-        self.saveHistoryString()
+    override var exampleModels: [TSRingModel] {
+        []
     }
     
-    static func saveHistoryString(){
-        if let jsonString = listModelArray.toJSONString() {
-            historyString = jsonString
-        }
+    override func saveModelAfterProcess() {
+        isHaveNew = true
     }
-    
-    
-    static var isHaveNew:Bool = false
 }
+

+ 2 - 2
AIRingtone/Business/TSAIPhotoVC/TSAIPhotoChildVC/TSAIPhotoChildVM.swift

@@ -37,9 +37,9 @@ class TSAIPhotoChildVM {
     
     var dataList:[TSActionInfoModel]{
         if style == .photo {
-            return TSPhotoHistory.listModelArray
+            return TSPhotoHistory.shared.listModels
         }else{
-            return TSPosterHistory.listModelArray
+            return TSPosterHistory.shared.listModels
         }
         
     }

+ 1 - 1
AIRingtone/Business/TSAIPhotoVC/TSGeneralPicBrowseVC/TSGeneralPhotoBrowseVC.swift

@@ -108,7 +108,7 @@ extension TSGeneralPhotoBrowseVC {
             guard let self = self else { return }
             contactsTool.setContactsAvatar(avatarImage: image) { data, error in
                 if error == nil {
-                    kSavePhotoSuccesswShared.show(atView: self.view,showViewBtn: false)
+                    kSaveSuccesswShared.show(atView: self.view,showViewBtn: false)
                 }
             }
         }

+ 3 - 3
AIRingtone/Business/TSAIPhotoVC/TSGeneralPicBrowseVC/TSGeneralPicBrowseVC.swift

@@ -139,7 +139,7 @@ class TSGeneralPicBrowseVC: TSBottomAlertVC {
         if let model = currentModel{
             //拷贝文字到截切板
             UIPasteboard.general.string = model.request.prompt
-            kSavePhotoSuccesswShared.show(atView: self.view,text: "Copy Successfully".localized,showViewBtn:false)
+            kSaveSuccesswShared.show(atView: self.view,text: "Copy Successfully".localized,showViewBtn:false)
         }else{
             kShowToastDataMissing()
         }
@@ -155,7 +155,7 @@ class TSGeneralPicBrowseVC: TSBottomAlertVC {
             if style == .poster {
                 PhotoManagerShared.saveImageToAlbum(image) { success, error in
                     if success {
-                        kSavePhotoSuccesswShared.show(atView:self.view)
+                        kSaveSuccesswShared.show(atView:self.view)
                     }else{
                         debugPrint(error)
                     }
@@ -163,7 +163,7 @@ class TSGeneralPicBrowseVC: TSBottomAlertVC {
             }else if style == .photo {
                 contactsTool.setContactsAvatar(avatarImage: image) { data, error in
                     if error == nil {
-                        kSavePhotoSuccesswShared.show(atView: self.view,showViewBtn: false)
+                        kSaveSuccesswShared.show(atView: self.view,showViewBtn: false)
                     }
                 }
             }

+ 1 - 1
AIRingtone/Business/TSAIPhotoVC/TSGeneralPicBrowseVC/TSGeneralPosterBrowseVC.swift

@@ -121,7 +121,7 @@ extension TSGeneralPosterBrowseVC {
                 if let image = image{
                     PhotoManagerShared.saveImageToAlbum(image) { success, error in
                         if success {
-                            kSavePhotoSuccesswShared.show(atView:self.view)
+                            kSaveSuccesswShared.show(atView:self.view)
                         }else{
                             debugPrint(error)
                         }

+ 2 - 2
AIRingtone/Business/TSAIPhotoVC/TSGeneralPicVC/TSGeneralPicVC.swift

@@ -104,7 +104,7 @@ class TSGeneralPicVC: TSBottomAlertVC {
             if gennerateType == .poster {
                 PhotoManagerShared.saveImageToAlbum(image) { success, error in
                     if success {
-                        kSavePhotoSuccesswShared.show(atView:self.view)
+                        kSaveSuccesswShared.show(atView:self.view)
                     }else{
                         debugPrint(error)
                     }
@@ -112,7 +112,7 @@ class TSGeneralPicVC: TSBottomAlertVC {
             }else if gennerateType == .photo {
                 contactsTool.setContactsAvatar(avatarImage: image) { data, error in
                     if error == nil {
-                        kSavePhotoSuccesswShared.show(atView: self.view,showViewBtn: false)
+                        kSaveSuccesswShared.show(atView: self.view,showViewBtn: false)
                     }
                 }
             }

+ 18 - 2
AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/Model/TSRingModel.swift

@@ -15,6 +15,9 @@ class TSRingModel:TSBaseModel {
     var duration:Int = 0
     var categoryId:String = ""
     
+    
+    var documentPath: String = ""
+    
     override func mapping(map: Map) {
         vip <- map["vip"]
         size <- map["size"]
@@ -22,11 +25,24 @@ class TSRingModel:TSBaseModel {
         audioUrl <- map["audioUrl"]
         duration <- map["duration"]
         categoryId <- map["categoryId"]
+        
+        documentPath <- map["documentPath"]
     }
     
+}
+
+
+extension TSRingModel {
+    
+    static func getTSRingModel(TSActionInfoModel model:TSActionInfoModel)->TSRingModel{
+        let ringModle = TSRingModel()
+        ringModle.title = model.response.title
+        ringModle.audioUrl = model.response.musicUrl
+        ringModle.duration = 30
+        ringModle.size = 30
+        return ringModle
+    }
     
-    var downloadOp:DownloadOperation?
-    var audioPlayerFileTool:TSAudioPlayerFileTool?
 }
 
 class TSRingCategoryModel:TSBaseModel {

+ 31 - 22
AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/TSAIRintoneVC.swift

@@ -13,6 +13,14 @@ class TSAIRintoneVC: TSBaseVC {
         return viewModel
     }()
 
+    lazy var redDot: UIView = {
+        let redDot = UIView()
+        redDot.backgroundColor = .themeColor
+        redDot.cornerRadius = 3
+        redDot.isHidden = true
+        return redDot
+    }()
+    
     lazy var navBarView: TSBaseNavContentBarView = {
         let navBarView = TSBaseNavContentBarView()
 
@@ -24,6 +32,26 @@ class TSAIRintoneVC: TSBaseVC {
             make.height.equalTo(48)
         }
 
+        let rightBtn = UIButton.createButton(image: UIImage(named: "ring_folder")){[weak self]  in
+            guard let self = self else { return }
+            kPushVC(target: self, modelVC: TSRingDownVC())
+        }
+        navBarView.barView.addSubview(rightBtn)
+        rightBtn.snp.makeConstraints { make in
+            make.trailing.equalTo(-16)
+            make.centerY.equalToSuperview()
+            make.width.equalTo(24)
+            make.height.equalTo(24)
+        }
+        
+        rightBtn.addSubview(redDot)
+        redDot.snp.makeConstraints { make in
+            make.trailing.equalTo(2)
+            make.top.equalTo(1)
+            make.width.equalTo(6)
+            make.height.equalTo(6)
+        }
+        
         return navBarView
     }()
 
@@ -108,6 +136,7 @@ class TSAIRintoneVC: TSBaseVC {
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
+        redDot.isHidden = !TSMineRintoneHistory.shared.isHaveNew
         viewModel.updateRecentData()
         updateListView()
     }
@@ -137,7 +166,7 @@ class TSAIRintoneVC: TSBaseVC {
             }
         }
         
-        TSAIRintoneHistory.dePrintAllModel()
+        TSAIRintoneHistory.shared.dePrintAllModel()
     }
     
     func updateListView(){
@@ -177,10 +206,7 @@ extension TSAIRintoneVC: UICollectionViewDataSource ,UICollectionViewDelegate,UI
             let itemModel = sectionModel.list.safeObj(At: indexPath.item),
            let cell = cell as? TSAIRintoneHistoryCell
         {
-            weak var weakSelf = self
-            cell.delegate = weakSelf
-            cell.setRingBtn.indexPath = indexPath
-            cell.setRingBtn.addTarget(self, action: #selector(clickSetRingBtn(_ :)), for: .touchUpInside)
+            cell.setTargetVC(targetVC: self, indexPath: indexPath)
             if let model = itemModel as? TSActionInfoModel {
                 cell.model = model
                 cell.ringModel = nil
@@ -193,23 +219,6 @@ extension TSAIRintoneVC: UICollectionViewDataSource ,UICollectionViewDelegate,UI
         return cell
     }
 
-    @objc func clickSetRingBtn(_ btn:TSUIExpandedTouchButton){
-        let indexPath = btn.indexPath
-        if let sectionModel = self.viewModel.modelList.safeObj(At: indexPath.section),
-        let model = sectionModel.list.safeObj(At: indexPath.item){
-            if let model = model as? TSActionInfoModel {
-                _ = kPurchaseToolShared.kshareBand(needVip: model.response.vip, vc: self, urlString: model.response.musicUrl, fileName: model.response.title)
-            }else if let ringModel = model as? TSRingModel {
-                _ = kPurchaseToolShared.kshareBand(needVip: ringModel.vip, vc: self, urlString: ringModel.audioUrl, fileName: ringModel.title){ success in
-                    if success {
-                        TSMineRintoneHistory.saveModel(model: ringModel)
-                    }
-                }
-            }
-        }
-        TSBusinessAudioPlayer.shared.stop()
-    }
-    
     public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
     }
     

+ 173 - 42
AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/View/TSAIRintoneHistoryCell.swift

@@ -7,11 +7,9 @@
 import Combine
 import SwipeCellKit
 class TSAIRintoneHistoryCell: SwipeCollectionViewCell  {
-    
-    public var colComponent:TSCollectionViewComponent?
-    public var colAttributes:[String : Any]?
-    public var cancellable: [AnyCancellable] = []
-    
+
+    weak var targetVC:UIViewController?
+    var indexPath:IndexPath = IndexPath(item: 0, section: 0)
     override init(frame: CGRect) {
         super.init(frame: frame)
         creatUI()
@@ -24,24 +22,29 @@ class TSAIRintoneHistoryCell: SwipeCollectionViewCell  {
 
     static let cellID = "TSAIRintoneHistoryCell"
 
-    lazy var setRingBtn: TSUIExpandedTouchButton = {
-        let setRingBtn = TSUIExpandedTouchButton()
-        setRingBtn.setUpButton(image: UIImage(named: "ai_setRing_icon"))
-        setRingBtn.setImage(UIImage(named: "ai_setRing_icon"), for: .normal)
-        return setRingBtn
-    }()
-    
     lazy var ringView: TSRingToneCellView = {
         let ringToneView = TSRingToneCellView()
         ringToneView.cornerRadius = 0.0
+        ringToneView.setRingBtn.isHidden = false
+        ringToneView.editBtn.isHidden = false
         ringToneView.clickPlayHandel = { [weak self] isplay in
             guard let self = self else { return }
             clickPlay()
         }
+        
+        ringToneView.clickEditHandel = { [weak self] in
+            guard let self = self else { return }
+            clickEidtBtn()
+        }
+        
+        ringToneView.clickSetUpHandel = { [weak self] in
+            guard let self = self else { return }
+            clickSetRingBtn()
+      
+        }
         return ringToneView
     }()
     
-    
     lazy var generateView: TSRingToneGenerateView = {
         let generateView = TSRingToneGenerateView()
         generateView.isHidden = true
@@ -104,13 +107,6 @@ class TSAIRintoneHistoryCell: SwipeCollectionViewCell  {
             make.edges.equalToSuperview()
         }
     
-        contentView.addSubview(setRingBtn)
-        setRingBtn.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalTo(-16)
-            make.width.height.equalTo(20)
-        }
-        
         contentView.addSubview(vipView)
         vipView.snp.makeConstraints { make in
             make.top.trailing.equalTo(0)
@@ -131,20 +127,27 @@ class TSAIRintoneHistoryCell: SwipeCollectionViewCell  {
         
     }
     
-    func dealThings(){
-        NotificationCenter.default.addObserver(forName: .kBusinessAudioStateChange, object: nil, queue: nil) {[weak self] notification in
-            guard let self = self else { return }
-            if let userInfo = notification.userInfo as? [String: TSBusinessAudioPlayer.PlayerState], let state = userInfo["PlayerState"] {
-                kExecuteOnMainThread {
-                    self.changePlayerState(state: state)
-                }
-            }
+    func setTargetVC(targetVC:UIViewController?,indexPath:IndexPath) {
+        self.targetVC = targetVC
+        self.indexPath = indexPath
+        
+        if let vc = targetVC as? any SwipeCollectionViewCellDelegate {
+          self.delegate = vc
         }
     }
     
+    func dealThings(){
+//        NotificationCenter.default.addObserver(forName: .kBusinessAudioStateChange, object: nil, queue: nil) { notification in
+//            if let userInfo = notification.userInfo as? [String: TSBusinessAudioPlayer.PlayerState], let state = userInfo["PlayerState"] {
+//                kExecuteOnMainThread {
+//                    self.changePlayerState(state: state)
+//                }
+//            }
+//        }
+    }
+    
     override func prepareForReuse() {
         super.prepareForReuse()
-//        cancellable.removeAll()
     }
     
     deinit {
@@ -169,6 +172,12 @@ extension TSAIRintoneHistoryCell {
 
         updataActionInfoModelView(model: model)
         dePrint("model actionStatus 收到=\(model.actionStatus)")
+        
+        if playSelf{
+            setPlayerStateChangedHandle()
+        }else{
+            changePlayerState(state: .stop)
+        }
     }
     
     func ringModelDidSet(){
@@ -180,6 +189,20 @@ extension TSAIRintoneHistoryCell {
         exampleView.isHidden = true
         generateView.isHidden = true
         vipView.isHidden = !ringModel.vip
+        
+        if playSelf{
+            setPlayerStateChangedHandle()
+        }else{
+            changePlayerState(state: .stop)
+        }
+    }
+    
+    func setPlayerStateChangedHandle(){
+        
+        TSBusinessAudioPlayer.shared.stateChangedHandle = { [weak self] playerState in
+            guard let self = self else { return }
+            self.changePlayerState(state: playerState)
+        }
         self.changePlayerState(state: TSBusinessAudioPlayer.shared.currentPlayerState)
     }
     
@@ -195,24 +218,26 @@ extension TSAIRintoneHistoryCell {
             if TSBusinessAudioPlayer.shared.isPlaying{
                 TSBusinessAudioPlayer.shared.stop()
             }else{
-                TSBusinessAudioPlayer.shared.playUrlString(modelUrlString,indexPath: indexPath)
+                TSBusinessAudioPlayer.shared.playUrlString(modelUrlString,localURL: modelLocalURL,indexPath: indexPath)
             }
             
         }else{
-            TSBusinessAudioPlayer.shared.playUrlString(modelUrlString,indexPath: indexPath)
+            TSBusinessAudioPlayer.shared.stop()//让上一个对象停止播放
+            setPlayerStateChangedHandle()//更换监听
+            TSBusinessAudioPlayer.shared.playUrlString(modelUrlString,localURL: modelLocalURL,indexPath: indexPath)
         }
     }
     
 
     func changePlayerState(state:TSBusinessAudioPlayer.PlayerState){
-        if playSelf == false {
-            if ringView.isPlay == true ||
-                ringView.isloading == true
-            {
-                ringView.handelAudioPlayerStateChange(state: .stop)
-            }
-            return
-        }
+//        if playSelf == false {
+//            if ringView.isPlay == true ||
+//                ringView.isloading == true
+//            {
+//                ringView.handelAudioPlayerStateChange(state: .stop)
+//            }
+//            return
+//        }
 //        dePrint("changePlayerState = \(state)")
         ringView.handelAudioPlayerStateChange(state: state)
     }
@@ -246,7 +271,7 @@ extension TSAIRintoneHistoryCell {
     }
 
     var playSelf:Bool{
-        return TSBusinessAudioPlayer.shared.isPlayURLString(string: modelUrlString,indexPath: indexPath)
+        return TSBusinessAudioPlayer.shared.isPlayURLString(string: modelUrlString,localURL: modelLocalURL,indexPath: indexPath)
     }
 
     var modelUrlString:String{
@@ -258,9 +283,115 @@ extension TSAIRintoneHistoryCell {
         }
         return urlString
     }
+
+    var modelLocalURL:URL?{
+        if let model = model{
+            return nil
+        }else if let ringModel = ringModel{
+            return TSDownloadManager.getRingLocalURL(ringModel: ringModel)
+        }
+        return nil
+    }
+    
+}
+//点击编辑和设置按钮
+extension TSAIRintoneHistoryCell {
+    
+    @objc func clickSetRingBtn(){
+        if let model = model {
+            setShareBand(infoModel: model)
+        }else if let ringModel = ringModel  {
+            setShareBand(ringModel: ringModel)
+        }
+    
+        TSBusinessAudioPlayer.shared.stop()
+    }
+    
+    func setShareBand(infoModel:TSActionInfoModel){
+        guard let targetVC = targetVC else { return }
+        let needVip = infoModel.response.vip
+        let urlString = infoModel.response.musicUrl
+        let fileName = infoModel.response.title
+
+        TSDownloadManager.getDownLoadRing(urlString: urlString){ url,downLoad in
+            if let path = url {
+                _ = kPurchaseToolShared.kshareBand(needVip: needVip, vc: targetVC, fileURL: path, fileName: fileName)
+            }
+        }
+    }
+    
+    func setShareBand(ringModel:TSRingModel){
+        guard let targetVC = targetVC else { return }
+        let needVip = ringModel.vip
+        let fileName = ringModel.title
+        
+        let kshareBand:(URL)->Void = { path in
+            _ = kPurchaseToolShared.kshareBand(needVip: needVip, vc: targetVC, fileURL: path, fileName: fileName){  success in
+                if success {
+                    var documentPath = path.path
+                    if path.path.isDocumentPath == false{
+                        let savePath = TSBusinessFileManager.saveRingPathURL.appendingPathComponent(path.lastPathComponent)
+                        TSFileManagerTool.copyFileWithOverwrite(from: path, to: savePath)
+                        documentPath = savePath.path
+                    }
+                    ringModel.documentPath = documentPath.documentLastURLString
+                    TSMineRintoneHistory.shared.replaceAndSaveModel(saveModel: ringModel) { ringModel, model in
+                        if model.title == ringModel.title,
+                           model.audioUrl == ringModel.audioUrl,
+                           model.categoryId == ringModel.categoryId,
+                           model.documentPath == ringModel.documentPath{
+                            return true
+                        }
+                        return false
+                    }
+                }
+            }
+        }
     
+        TSDownloadManager.getDownLoadRing(ringModel: ringModel) { url, downLoad in
+            if let path = url {
+                kshareBand(path)
+            }
+        }
+    }
     
-    var indexPath:IndexPath? {
-        return setRingBtn.indexPath
+    @objc func clickEidtBtn(){
+
+        guard let targetVC = targetVC else { return }
+        var editRingModel:TSRingModel? = nil
+        if let model = self.ringModel {
+            editRingModel = model
+        }else if let model = self.model {
+            editRingModel = TSRingModel.getTSRingModel(TSActionInfoModel: model)
+        }
+
+        if let editRingModel = editRingModel,let copyRingModel = editRingModel.copy() as? TSRingModel{
+            let urlString = copyRingModel.audioUrl
+            TSDownloadManager.getDownLoadRing(ringModel: copyRingModel)
+            { [weak self] url, downLoad in
+                guard let self = self else { return }
+                if let path = url {
+                    pushVCTSEditAudioVideo(targetVC: targetVC, modelVC: TSEditAudioVC(ringModel: copyRingModel, editOriginalURL: path))
+                }else{
+                    kShowToastDataMissing()
+                }
+            }
+        }
+    }
+    
+    func pushVCTSEditAudioVideo(targetVC:UIViewController,modelVC:TSEditAudioVC){
+    
+        if let fileInfo = TSBusinessAudioPlayer.getAudioFileInfo(path: modelVC.editOriginalURL.path) {
+            if let size = fileInfo.sizeInBytes {
+                modelVC.ringModel.size = Int(size)
+            }
+            
+            if let duration = fileInfo.durationInSeconds {
+                modelVC.ringModel.duration = Int(duration)
+            }
+        }
+        
+        TSBusinessAudioPlayer.shared.stop()
+        kPushVC(target: targetVC, modelVC: modelVC)
     }
 }

+ 3 - 3
AIRingtone/Business/TSAIRintoneVC/TSAIRintoneVC/ViewModel/TSAIRintoneVM.swift

@@ -44,7 +44,7 @@ class TSAIRintoneVM {
     
     //生成的历史
     lazy var aiRintoneHistoryModel: TSAIRintoneHistoryModel = {
-        let model = TSAIRintoneHistoryModel(title: "Generate History", list:Array(TSAIRintoneHistory.listModelArray.prefix(2)),type: .history)
+        let model = TSAIRintoneHistoryModel(title: "Generate History", list:Array(TSAIRintoneHistory.shared.listModels.prefix(2)),type: .history)
         return model
     }()
     
@@ -61,12 +61,12 @@ class TSAIRintoneVM {
 extension TSAIRintoneVM {
     
     func updateRecentData() {
-        aiRintoneHistoryModel.list = Array(TSAIRintoneHistory.listModelArray.prefix(2))
+        aiRintoneHistoryModel.list = Array(TSAIRintoneHistory.shared.listModels.prefix(2))
         modelList = getModelList()
     }
     
     func removeModel(model:TSActionInfoModel){
-        TSAIRintoneHistory.removeModel(model: model)
+        TSAIRintoneHistory.shared.removeModel(model: model)
         updateRecentData()
     }
     

+ 0 - 4
AIRingtone/Business/TSAIRintoneVC/TSGeneralRintoneVC/TSGeneralRintoneVC+Event.swift

@@ -42,12 +42,8 @@ extension TSGeneralRintoneVC {
         updateInfoModel(model: model)
         switch state {
             case .failed(let errorStr):
-                audioPlayer.stop()
-//                hiddenSelfVC(hidden: false)
                 showError(text: errorStr)
             case .success:
-                audioPlayer.stop()
-//                hiddenSelfVC(hidden: false)
                 if let model = model {
                     showSuccess(model: model)
                 }else{

+ 24 - 7
AIRingtone/Business/TSAIRintoneVC/TSGeneralRintoneVC/TSGeneralRintoneVC.swift

@@ -110,15 +110,32 @@ class TSGeneralRintoneVC: TSBottomAlertVC {
             return
         }
         audioPlayer.stop()
-        _ = kPurchaseToolShared.kshareBand(needVip: model.response.vip, vc: self, urlString: model.response.musicUrl, fileName: model.response.title)
+        TSDownloadManager.getDownLoadRing(urlString: model.response.musicUrl){ url,downLoad in
+            if let path = url {
+                _ = kPurchaseToolShared.kshareBand(needVip: model.response.vip, vc: self, fileURL: path, fileName: model.response.title)
+            }
+        }
     }
     
     @objc func clickPlay(){
-        if audioPlayer.isPlaying{
-            audioPlayer.stop()
-        }else{
-            audioPlayer.playUrlString( infoModel?.response.musicUrl)
+        
+        guard let infoModel = infoModel else { return }
+        
+        if  TSBusinessAudioPlayer.shared.isPlayURLString(string: infoModel.response.musicUrl) {
+            if TSBusinessAudioPlayer.shared.isLoading{
+                return
+            }
+            if TSBusinessAudioPlayer.shared.isPlaying{
+                TSBusinessAudioPlayer.shared.stop()
+            }else{
+                audioPlayer.playUrlString(infoModel.response.musicUrl)
+            }
+        }else {
+            TSBusinessAudioPlayer.shared.stop()//让上一个对象停止播放
+            ringView.monitorPlayStateDefaultHandle()//更换监听
+            audioPlayer.playUrlString(infoModel.response.musicUrl)
         }
+
     }
     
     override func viewWillDisappear(_ animated: Bool) {
@@ -140,13 +157,13 @@ class TSGeneralRintoneVC: TSBottomAlertVC {
         }else {
             creatRintone()
         }
-        ringView.monitorPlayStateDefaultHandle()
+    
     }
     
     func creatRintone() {
         if kPurchaseToolShared.kJudgeVipFreeType(vipFreeNumType: .ringtones, vc: self){ return }//判断 vip
         self.uuidString = UUID().uuidString
-        let operation:TSGenerateRintoneOperation = TSGenerateRintoneOperationQueue.shared.creatOperation(uuid: self.uuidString) as! TSGenerateRintoneOperation
+        let operation:TSGenerateRintoneOperation = TSGenerateRintoneOperationQueue.shared.creatOperation(uuid: self.uuidString) 
         operation.$stateDatauPblished.receive(on: DispatchQueue.main).sink {[weak self]  (state,model) in
             guard let self = self else { return }
             self.upDateView(state: state, model: model)

+ 3 - 22
AIRingtone/Business/TSAIRintoneVC/TSGenerateHistoryVC/TSGenerateHistoryVC.swift

@@ -119,13 +119,12 @@ extension TSGenerateHistoryVC: UICollectionViewDataSource ,UICollectionViewDeleg
             let itemModel = sectionModel.list.safeObj(At: indexPath.item),
            let cell = cell as? TSAIRintoneHistoryCell
         {
-            weak var weakSelf = self
-            cell.delegate = weakSelf
-            cell.setRingBtn.indexPath = indexPath
-            cell.setRingBtn.addTarget(weakSelf, action: #selector(clickSetRingBtn(_ :)), for: .touchUpInside)
+            cell.setTargetVC(targetVC: self, indexPath: indexPath)
             if let model = itemModel as? TSActionInfoModel {
                 cell.model = model
+                cell.ringModel = nil
             }else if let ringModel = itemModel as? TSRingModel {
+                cell.model = nil
                 cell.ringModel = ringModel
             }
         }
@@ -133,24 +132,6 @@ extension TSGenerateHistoryVC: UICollectionViewDataSource ,UICollectionViewDeleg
         return cell
     }
 
-    @objc func clickSetRingBtn(_ btn:TSUIExpandedTouchButton){
-        let indexPath = btn.indexPath
-        if let sectionModel = self.viewModel.modelList.safeObj(At: indexPath.section),
-        let model = sectionModel.list.safeObj(At: indexPath.item){
-            if let model = model as? TSActionInfoModel {
-                _ = kPurchaseToolShared.kshareBand(needVip: model.response.vip, vc: self, urlString: model.response.musicUrl, fileName: model.response.title)
-            }else if let ringModel = model as? TSRingModel {
-                _ = kPurchaseToolShared.kshareBand(needVip: ringModel.vip, vc: self, urlString: ringModel.audioUrl, fileName: ringModel.title)
-//                { success in
-//                    if success {
-//                        TSMineRintoneHistory.saveModel(model: ringModel)
-//                    }
-//                }
-            }
-        }
-        TSBusinessAudioPlayer.shared.stop()
-    }
-    
     public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
         
     }

+ 3 - 3
AIRingtone/Business/TSAIRintoneVC/TSGenerateHistoryVC/TSGenerateHistoryVM.swift

@@ -24,7 +24,7 @@ class TSGenerateHistoryVM {
     
     //生成的历史
     lazy var aiRintoneHistoryModel: TSAIRintoneHistoryModel = {
-        let model = TSAIRintoneHistoryModel(title: "Generate History", list:TSAIRintoneHistory.listModelArray)
+        let model = TSAIRintoneHistoryModel(title: "Generate History", list:TSAIRintoneHistory.shared.listModels)
         return model
     }()
 
@@ -34,12 +34,12 @@ class TSGenerateHistoryVM {
 extension TSGenerateHistoryVM {
     
     func updateRecentData() {
-        aiRintoneHistoryModel.list = TSAIRintoneHistory.listModelArray
+        aiRintoneHistoryModel.list = TSAIRintoneHistory.shared.listModels
         modelList = getModelList()
     }
     
     func removeModel(model:TSActionInfoModel){
-        TSAIRintoneHistory.removeModel(model: model)
+        TSAIRintoneHistory.shared.removeModel(model: model)
         updateRecentData()
     }
     

+ 6 - 5
AIRingtone/Business/TSCollectionViewVM/TSCollectionViewVM+Config.swift

@@ -5,6 +5,7 @@
 //  Created by 100Years on 2025/2/28.
 //
 
+private let widthReduction:CGFloat = 1.0
 
 struct TSColVVMSizeConfig {
     
@@ -41,7 +42,7 @@ let themeGuideConfig:TSColVVMSizeConfig = {
     let cellRowNum = 1
     
     let originalScale = originalSize.width/originalSize.height
-    var w = k_ScreenWidth-sectionInset.left-sectionInset.right
+    var w = k_ScreenWidth-sectionInset.left-sectionInset.right-widthReduction
     w = w - lineSpacing * CGFloat((cellRowNum-1))
     w = w/CGFloat(cellRowNum)
     let h = w/originalScale
@@ -84,7 +85,7 @@ let themeContentConfig:TSColVVMSizeConfig = {
     let cellRowNum = 2
     
     let originalScale = originalSize.width/originalSize.height
-    var w = k_ScreenWidth-sectionInset.left-sectionInset.right
+    var w = k_ScreenWidth-sectionInset.left-sectionInset.right-widthReduction
     w = w - lineSpacing * CGFloat((cellRowNum-1))
     w = w/CGFloat(cellRowNum)
     let h = w/originalScale
@@ -128,7 +129,7 @@ let posterHistoryConfig:TSColVVMSizeConfig = {
     let cellRowNum = 3
     
     let originalScale = originalSize.width/originalSize.height
-    var w = k_ScreenWidth-sectionInset.left-sectionInset.right
+    var w = k_ScreenWidth-sectionInset.left-sectionInset.right-widthReduction
     w = w - lineSpacing * CGFloat((cellRowNum-1))
     w = w/CGFloat(cellRowNum)
     let h = w/originalScale
@@ -171,7 +172,7 @@ let photoHistoryConfig:TSColVVMSizeConfig = {
     let cellRowNum = 3
     
     let originalScale = originalSize.width/originalSize.height
-    var w = k_ScreenWidth-sectionInset.left-sectionInset.right
+    var w = k_ScreenWidth-sectionInset.left-sectionInset.right-widthReduction
     w = w - lineSpacing * CGFloat((cellRowNum-1))
     w = w/CGFloat(cellRowNum)
     let h = w/originalScale
@@ -214,7 +215,7 @@ let ringCategoriesConfig:TSColVVMSizeConfig = {
     let cellRowNum = 3
     
     let originalScale = originalSize.width/originalSize.height
-    var w = k_ScreenWidth-sectionInset.left-sectionInset.right
+    var w = k_ScreenWidth-sectionInset.left-sectionInset.right-widthReduction
     w = w - lineSpacing * CGFloat((cellRowNum-1))
     w = w/CGFloat(cellRowNum)
     let h = w/originalScale

+ 1 - 20
AIRingtone/Business/TSDiscoverVC/TSDiscoverListVC/TSDiscoverListVC.swift

@@ -136,8 +136,7 @@ extension TSDiscoverListVC: UICollectionViewDataSource ,UICollectionViewDelegate
             let itemModel = sectionModel.list.safeObj(At: indexPath.item),
            let cell = cell as? TSAIRintoneHistoryCell
         {
-            cell.setRingBtn.indexPath = indexPath
-            cell.setRingBtn.addTarget(self, action: #selector(clickSetRingBtn(_ :)), for: .touchUpInside)
+            cell.setTargetVC(targetVC: self, indexPath: indexPath)
             if let model = itemModel as? TSActionInfoModel {
                 cell.model = model
                 cell.ringModel = nil
@@ -149,22 +148,4 @@ extension TSDiscoverListVC: UICollectionViewDataSource ,UICollectionViewDelegate
         
         return cell
     }
-
-    @objc func clickSetRingBtn(_ btn:TSUIExpandedTouchButton){
-        let indexPath = btn.indexPath
-        if let sectionModel = self.viewModel.modelList.safeObj(At: indexPath.section),
-        let itemModel = sectionModel.list.safeObj(At: indexPath.item){
-//            if let model = itemModel as? TSActionInfoModel {
-//                _ = kPurchaseToolShared.kshareBand(needVip: model.response.vip, vc: self, urlString: model.response.musicUrl, fileName: model.response.title)
-//            }else
-            if let ringModel = itemModel as? TSRingModel {
-                _ = kPurchaseToolShared.kshareBand(needVip: ringModel.vip, vc: self, urlString: ringModel.audioUrl, fileName: ringModel.title){ success in
-                    if success {
-                        TSMineRintoneHistory.saveModel(model: ringModel)
-                    }
-                }
-            }
-        }
-        TSBusinessAudioPlayer.shared.stop()
-    }
 }

+ 1 - 1
AIRingtone/Business/TSDiscoverVC/TSDiscoverVC/TSDiscoverVC.swift

@@ -117,6 +117,6 @@ class TSDiscoverVC: TSBaseVC {
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
         
-        redDot.isHidden = !TSMineRintoneHistory.isHaveNew
+        redDot.isHidden = !TSMineRintoneHistory.shared.isHaveNew
     }
 }

+ 13 - 30
AIRingtone/Business/TSDiscoverVC/TSRingDownVC/TSRingDownVC.swift

@@ -18,8 +18,8 @@ class TSRingDownVC: TSBaseVC {
         let vc = TSDiscoverListVC(ringCategoryModel: ringCategoryModel)
         vc.reloadUIBlock = { [weak self]  in
             guard let self = self else { return }
-            viewModel.updateRecentData()
-            updateListView()
+//            viewModel.updateRecentData()
+//            updateListView()
         }
         kPushVC(target: self, modelVC:vc)
     }
@@ -79,7 +79,9 @@ class TSRingDownVC: TSBaseVC {
     
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
-        TSMineRintoneHistory.isHaveNew = false
+        TSMineRintoneHistory.shared.isHaveNew = false
+        viewModel.updateRecentData()
+        updateListView()
     }
     
     override func viewWillDisappear(_ animated: Bool) {
@@ -88,7 +90,7 @@ class TSRingDownVC: TSBaseVC {
     }
     
     override func dealThings() {
-        updateListView()
+//        updateListView()
     }
     
     override func navBarClickLeftAction() {
@@ -123,10 +125,7 @@ extension TSRingDownVC: UICollectionViewDataSource ,UICollectionViewDelegate,UIC
             let itemModel = sectionModel.list.safeObj(At: indexPath.item),
            let cell = cell as? TSAIRintoneHistoryCell
         {
-            weak var weakSelf = self
-            cell.delegate = weakSelf
-            cell.setRingBtn.indexPath = indexPath
-            cell.setRingBtn.addTarget(self, action: #selector(clickSetRingBtn(_ :)), for: .touchUpInside)
+            cell.setTargetVC(targetVC: self, indexPath: indexPath)
             if let model = itemModel as? TSActionInfoModel {
                 cell.model = model
                 cell.ringModel = nil
@@ -139,27 +138,6 @@ extension TSRingDownVC: UICollectionViewDataSource ,UICollectionViewDelegate,UIC
         
         return cell
     }
-
-    @objc func clickSetRingBtn(_ btn:TSUIExpandedTouchButton){
-        let indexPath = btn.indexPath
-        if let sectionModel = self.viewModel.modelList.safeObj(At: indexPath.section),
-        let model = sectionModel.list.safeObj(At: indexPath.item){
-//            if let model = model as? TSActionInfoModel {
-//                _ = kPurchaseToolShared.kshareBand(needVip: model.response.vip, vc: self, urlString: model.response.musicUrl, fileName: model.response.title)
-//            }else
-
-            if let ringModel = model as? TSRingModel {
-                _ = kPurchaseToolShared.kshareBand(needVip: ringModel.vip, vc: self, urlString: ringModel.audioUrl, fileName: ringModel.title)
-//                { success in
-//                    if success {
-//                        TSMineRintoneHistory.saveModel(model: ringModel)
-//                    }
-//                }
-            }
-        }
-        
-        TSBusinessAudioPlayer.shared.stop()
-    }
     
     public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
         
@@ -179,6 +157,11 @@ extension TSRingDownVC: SwipeCollectionViewCellDelegate {
                 if let sectionModel = self.viewModel.modelList.safeObj(At: indexPath.section),
                 let model = sectionModel.list.safeObj(At: indexPath.item){
                     if let model = model as? TSRingModel {
+                        
+                        if TSBusinessAudioPlayer.shared.isPlayURLString(string: model.audioUrl,localURL:model.documentPath.fillDocumentURL) {
+                            TSBusinessAudioPlayer.shared.stop()
+                        }
+                        
                         collectionView.performBatchUpdates({
                             self.viewModel.removeModel(model: model)
                             if sectionModel.list.count == 0 {
@@ -187,7 +170,7 @@ extension TSRingDownVC: SwipeCollectionViewCellDelegate {
                                 collectionView.deleteItems(at: [indexPath])
                             }
                         })
-                        TSBusinessAudioPlayer.shared.stop()
+
                         self.nullView.isHidden = self.viewModel.modelList.count > 0
                     }
                 }

+ 5 - 8
AIRingtone/Business/TSDiscoverVC/TSRingDownVC/VM/TSRingDownVM.swift

@@ -24,7 +24,7 @@ class TSRingDownVM {
     
     //保存的历史
     lazy var aiRintoneHistoryModel: TSAIRintoneHistoryModel = {
-        let model = TSAIRintoneHistoryModel(title: "Generate History", list:TSMineRintoneHistory.listModelArray)
+        let model = TSAIRintoneHistoryModel(title: "Generate History", list:TSMineRintoneHistory.shared.listModels)
         return model
     }()
     
@@ -33,18 +33,15 @@ class TSRingDownVM {
 extension TSRingDownVM {
     
     func updateRecentData() {
-        aiRintoneHistoryModel.list = TSMineRintoneHistory.listModelArray
+        aiRintoneHistoryModel.list = TSMineRintoneHistory.shared.listModels
         modelList = getModelList()
     }
     
     func removeModel(model:TSRingModel){
-        TSMineRintoneHistory.removeModel(model: model)
+        let removePath = model.documentPath.fillDocumentURL
+        TSMineRintoneHistory.shared.removeModel(model: model)
         updateRecentData()
-        
-        if let pathURL = TSCommonTool.getCachedURLString(from: model.audioUrl) {
-            TSFileManagerTool.removeItem(from: pathURL)
-        }
-
+        TSFileManagerTool.removeItem(from: removePath)
     }
     
 }

+ 55 - 0
AIRingtone/Business/TSEditAudioVideoVC/TSEditAudioSliderView.swift

@@ -0,0 +1,55 @@
+//
+//  TSEditAudioSliderView.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/27.
+//
+
+class TSEditAudioSliderView: TSBaseView {
+    
+    lazy var leftLabel: UILabel = {
+        let leftLabel = UILabel.createLabel(text: "00:00s",font: .font(size: 14.0),textColor: .white.withAlphaComponent(0.8),numberOfLines: 2)
+        return leftLabel
+    }()
+    
+    lazy var slider: UISlider = {
+        let slider = UISlider()
+        slider.setThumbImage(UIImage(named: "ic-ring-edit-slider"), for: .normal)
+        slider.thumbTintColor = .themeColor
+        slider.minimumTrackTintColor = .themeColor
+        slider.maximumTrackTintColor = .white.withAlphaComponent(0.1)
+        return slider
+    }()
+    
+    lazy var rightLabel: UILabel = {
+        let leftLabel = UILabel.createLabel(text: "00:00s",font: .font(size: 14.0),textColor: .white.withAlphaComponent(0.8),textAlignment: .right,numberOfLines: 2)
+        return leftLabel
+    }()
+    
+    override func creatUI() {
+        self.frame = CGRectMake(0, 0, k_ScreenWidth, 40)
+        contentView.addSubview(leftLabel)
+        contentView.addSubview(slider)
+        contentView.addSubview(rightLabel)
+        
+        leftLabel.snp.makeConstraints { make in
+            make.leading.equalTo(16)
+            make.centerY.equalToSuperview()
+            make.width.equalTo(62)
+        }
+        
+        slider.snp.makeConstraints { make in
+            make.leading.equalTo(87)
+            make.trailing.equalTo(-57)
+            make.centerY.equalToSuperview()
+        }
+        
+        rightLabel.snp.makeConstraints { make in
+            make.trailing.equalTo(-16)
+            make.centerY.equalToSuperview()
+            make.width.equalTo(40)
+        }
+    }
+    
+    
+}

+ 825 - 0
AIRingtone/Business/TSEditAudioVideoVC/TSEditAudioVC.swift

@@ -0,0 +1,825 @@
+//
+//  TSEditAudioVC.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/30.
+//
+import AVFoundation
+class TSEditAudioVC: TSEditAudioVideoBaseVC , ZHCroppedDelegate, ZHWaveformViewDelegate {
+    //#################################### 数据区域 ####################################//
+    override var titleName:String{
+        "Edit Ringtone".localized
+    }
+    
+    var ringModel: TSRingModel //请传递副本过来,会修改数据的
+
+    init(ringModel: TSRingModel,editOriginalURL:URL) {
+        self.ringModel = ringModel
+        super.init(editOriginalURL: editOriginalURL)
+    }
+    
+    @MainActor required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    lazy var ringDuration:Double = Double(ringModel.duration)
+    //#################################### 名字 ####################################//
+    lazy var nameLabel: UILabel = {
+        let nameLabel = UILabel.createLabel(text: ringModel.title, font: .font(size: 14),textColor: .white.withAlphaComponent(0.8),textAlignment: .center)
+        return nameLabel
+    }()
+    
+    weak var nameInputTextField: UITextField?
+    lazy var nameButton: UIButton = {
+        let nameButton = UIButton.createButton(image: UIImage(named: "edit_field")) { [weak self]  in
+            guard let self = self else { return }
+            
+            let alertVC = UIAlertController(title: nil, message: "Ringtone Name".localized, preferredStyle: .alert)
+            alertVC.addTextField { textField in
+                textField.placeholder = "input name".localized
+                textField.font = UIFont.systemFont(ofSize: 16)
+                textField.text = self.ringModel.title
+                self.nameInputTextField = textField
+            }
+            let ok = UIAlertAction(title: "OK".localized, style: .default) { [weak self] _ in
+                guard let self = self else { return }
+                nameLabel.text = nameInputTextField?.text
+                
+                if let text = nameLabel.text{
+                    ringModel.title = text
+                }
+            }
+            let cancel = UIAlertAction(title: "Cancel".localized, style: .cancel)
+            alertVC.addAction(cancel)
+            alertVC.addAction(ok)
+            present(alertVC, animated: true) {
+                self.nameInputTextField?.becomeFirstResponder()
+            }
+            
+        }
+        return nameButton
+    }()
+    
+    lazy var nameView: UIView = {
+        let nameView = UIView()
+        
+        let nameContentView = UIView()
+        nameView.addSubview(nameContentView)
+        nameContentView.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        nameContentView.addSubview(nameLabel)
+        nameContentView.addSubview(nameButton)
+        
+        nameLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview()
+            make.width.lessThanOrEqualTo(220*kDesignScale)
+            make.height.equalTo(23)
+            make.top.bottom.equalTo(0)
+        }
+        nameButton.snp.makeConstraints { make in
+            make.trailing.equalToSuperview()
+            make.leading.equalTo(nameLabel.snp.trailing)
+            make.width.height.equalTo(23)
+            make.top.bottom.equalTo(0)
+        }
+        
+        return nameView
+    }()
+    
+    //#################################### track编辑视频 ####################################//\
+    let trackContentViewLeft:CGFloat = 24.0
+    let trackViewH:CGFloat = 210.0
+    lazy var trackViewW:CGFloat = k_ScreenWidth - trackContentViewLeft * 2
+    lazy var trackView: ZHWaveformView = {
+        let waveform = ZHWaveformView(frame: CGRect(x: 0, y: 0, width: trackViewW, height:trackViewH),fileURL: editOriginalURL)
+        waveform.backgroundColor = .clear
+        waveform.beginningPartColor = .white.withAlphaComponent(0.2) // color
+        waveform.endPartColor = .white.withAlphaComponent(0.2)
+        waveform.wavesColor = "#7E57F4".uiColor
+        waveform.trackScale = 0.2// 0 ~ 1
+        waveform.waveformDelegate = self
+        waveform.croppedDelegate = self
+        
+        return waveform
+    }()
+    
+    lazy var trackContentView: UIView = {
+        let trackContentView = UIView(frame: CGRectMake(24, 0, k_ScreenWidth-48, trackViewH+12))
+
+        trackContentView.addSubview(trackView)
+        trackView.snp.makeConstraints { make in
+            make.top.centerX.equalToSuperview()
+            make.width.equalTo(trackView.width)
+            make.height.equalTo(trackView.height)
+        }
+        return trackContentView
+    }()
+    
+    
+    func creatDragView()->UIView{
+        let bgView = UIView()
+        
+        let view1 = UIView.creatColor(color: .white)
+        bgView.addSubview(view1)
+        let view2 = UIView.creatColor(color: .white)
+        view2.cornerRadius = 6
+        bgView.addSubview(view2)
+        
+        view1.snp.makeConstraints { make in
+            make.width.equalTo(2)
+            make.height.equalTo(trackViewH)
+            make.centerX.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        view2.snp.makeConstraints { make in
+            make.top.equalTo(view1.snp.bottom)
+            make.width.height.equalTo(12)
+            make.bottom.equalToSuperview()
+            make.leading.equalTo(12)
+            make.trailing.equalTo(-12)
+        }
+        return bgView
+    }
+    lazy var dragLeftView: UIView = creatDragView()
+    lazy var dragRightView: UIView = creatDragView()
+    var progressLine: UIView = UIView.creatColor(color: .themeColor)
+    
+    
+    lazy var startTimeLabel: UILabel = {
+        let label = UILabel.createLabel(text: "00:00s",font: .font(size: 14.0),textColor: .lesserText)
+        return label
+    }()
+    var endTimeLabel: UILabel = {
+        let label = UILabel.createLabel(text: "10:00s",font: .font(size: 14.0),textColor: .lesserText)
+        return label
+    }()
+    var timeLabel: UILabel = {
+        let label = UILabel.createLabel(text: "00:00",font: .font(size: 26.0),textColor: .lesserText)
+        return label
+    }()
+    
+    
+    lazy var timeView: UIView = {
+        let timeView = UIView()
+        
+        timeView.addSubview(startTimeLabel)
+        timeView.addSubview(endTimeLabel)
+        timeView.addSubview(timeLabel)
+        
+        startTimeLabel.snp.makeConstraints { make in
+            make.leading.equalTo(16)
+            make.top.equalTo(12)
+            make.height.equalTo(20)
+        }
+        
+        endTimeLabel.snp.makeConstraints { make in
+            make.trailing.equalTo(-16)
+            make.top.equalTo(12)
+            make.height.equalTo(20)
+        }
+        
+        timeLabel.snp.makeConstraints { make in
+            make.centerX.equalToSuperview()
+            make.top.equalTo(45)
+            make.height.equalTo(26)
+        }
+        
+        return timeView
+    }()
+    
+    lazy var trackBgView: UIView  = {
+        let trackBgView = UIView()
+        
+        let leftPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.leftPanRecognizer(sender:)))
+        dragLeftView.addGestureRecognizer(leftPanRecognizer)
+        dragLeftView.isUserInteractionEnabled = true
+
+        let rightPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.rightPanRecognizer(sender:)))
+        dragRightView.addGestureRecognizer(rightPanRecognizer)
+        dragRightView.isUserInteractionEnabled = true
+        
+        let gradientImageView = UIImageView.createImageView(imageName: "trackBg_gradient",contentMode: .scaleToFill)
+        trackBgView.addSubview(gradientImageView)
+        gradientImageView.snp.makeConstraints { make in
+            make.leading.top.trailing.equalToSuperview()
+            make.height.equalTo(trackViewH)
+        }
+
+        trackBgView.addSubview(trackContentView)
+        trackContentView.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.leading.equalTo(trackContentView.x)
+            make.trailing.equalTo(-trackContentView.x)
+            make.height.equalTo(trackViewH)
+        }
+        
+   
+        trackBgView.addSubview(dragLeftView)
+        dragLeftView.snp.makeConstraints { make in
+            make.top.leading.equalToSuperview()
+        }
+        
+        trackBgView.addSubview(dragRightView)
+        dragRightView.snp.makeConstraints { make in
+            make.top.equalToSuperview()
+            make.trailing.equalToSuperview()
+        }
+        progressLine.isHidden = true
+        trackBgView.addSubview(progressLine)
+        progressLine.snp.makeConstraints { make in
+            make.top.leading.equalToSuperview()
+            make.width.equalTo(1)
+            make.height.equalTo(trackViewH)
+        }
+        
+        trackBgView.addSubview(timeView)
+        timeView.snp.makeConstraints { make in
+            make.top.equalTo(trackContentView.bottom)
+            make.trailing.leading.equalToSuperview()
+            make.height.equalTo(71)
+            make.bottom.equalTo(0)
+        }
+        
+        return trackBgView
+    }()
+    
+
+    //#################################### Fade in ####################################//
+    lazy var fadeinSliderView: TSEditAudioSliderView = {
+        let fadeinSliderView = TSEditAudioSliderView()
+        fadeinSliderView.leftLabel.text = "Fade in".localized
+        fadeinSliderView.rightLabel.text = "0s"
+        fadeinSliderView.slider.maximumValue = 5
+//        fadeinSliderView.slider.value = 3
+        return fadeinSliderView
+    }()
+    lazy var fadeinSlider: UISlider = {
+        let slider = fadeinSliderView.slider
+        slider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
+        slider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
+        slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
+        return slider
+    }()
+    //#################################### Fade out ####################################//
+    
+    lazy var fadeoutSliderView: TSEditAudioSliderView = {
+        let fadeoutSliderView = TSEditAudioSliderView()
+        fadeoutSliderView.leftLabel.text = "Fade out".localized
+        fadeoutSliderView.rightLabel.text = "0s"
+        fadeoutSliderView.slider.maximumValue = 5
+//        fadeoutSliderView.slider.value = 3
+        return fadeoutSliderView
+    }()
+    lazy var fadeoutSlider: UISlider = {
+        let slider = fadeoutSliderView.slider
+        slider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
+        slider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
+        slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
+        return slider
+    }()
+    //#################################### Output Volume ####################################//
+    lazy var outputVolumeSliderView: TSEditAudioSliderView = {
+        let outputVolumeSliderView = TSEditAudioSliderView()
+        outputVolumeSliderView.leftLabel.text = "Output Volume".localized
+        outputVolumeSliderView.rightLabel.text = "100%"
+        outputVolumeSliderView.slider.minimumValue = 0.2
+        outputVolumeSliderView.slider.maximumValue = 2.0
+        outputVolumeSliderView.slider.value = 1.0
+        return outputVolumeSliderView
+    }()
+    lazy var outputVolumeSlider: UISlider = {
+        let slider = outputVolumeSliderView.slider
+        slider.addTarget(self, action: #selector(sliderBeginTap(_:)), for: .touchDown)
+        slider.addTarget(self, action: #selector(sliderEndTap(_:)), for: .touchUpInside)
+        slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
+        return slider
+    }()
+    
+
+    
+
+    //#################################### viewDidLoad####################################//
+    override func createData() {
+
+    }
+    
+    override func createView() {
+        super.createView()
+        setUpCusStackView()
+    }
+    
+    func setUpCusStackView() {
+        cusStackView.addSubviewToStack(nameView)
+        nameView.snp.makeConstraints { make in
+            make.height.equalTo(56)
+            make.width.equalTo(k_ScreenWidth)
+        }
+        cusStackView.addSubviewToStack(trackBgView)
+        trackBgView.snp.makeConstraints { make in
+            make.height.equalTo(300)
+            make.width.equalTo(k_ScreenWidth)
+        }
+        
+        cusStackView.addSubviewToStack(playButtonView)
+        playButtonView.snp.makeConstraints { make in
+            make.height.equalTo(playButtonView.height)
+            make.width.equalTo(playButtonView.width)
+        }
+        
+        cusStackView.addSubviewToStack(fadeinSliderView)
+        fadeinSliderView.snp.makeConstraints { make in
+            make.height.equalTo(fadeinSliderView.height)
+        }
+        
+        let spaceView = UIView()
+        cusStackView.addSubviewToStack(spaceView)
+        spaceView.snp.makeConstraints { make in
+            make.height.equalTo(8)
+        }
+        
+        cusStackView.addSubviewToStack(fadeoutSliderView)
+        fadeoutSliderView.snp.makeConstraints { make in
+            make.height.equalTo(fadeinSliderView.height)
+        }
+        
+        let spaceView1 = UIView()
+        cusStackView.addSubviewToStack(spaceView1)
+        spaceView1.snp.makeConstraints { make in
+            make.height.equalTo(8)
+        }
+        
+        cusStackView.addSubviewToStack(outputVolumeSliderView)
+        outputVolumeSliderView.snp.makeConstraints { make in
+            make.height.equalTo(fadeinSliderView.height)
+            make.bottom.equalToSuperview()
+        }
+    
+        _ = fadeinSlider
+        _ = fadeoutSlider
+        _ = outputVolumeSlider
+    }
+    
+    override func dealThings() {
+        
+        dePrint("TSEditAudioVC ringModel = \(String(describing: ringModel.toJSONString())),editOriginalURL = \(editOriginalURL)")
+        
+        if player.duration != 0 {
+            ringDuration = player.duration
+            if ringModel.duration == 0{
+                ringModel.duration = Int(player.duration)
+            }
+        }
+
+        operationCache.append(ringModel)
+        reloadTrackView()
+    }
+
+    private lazy var player:TSBusinessAudioPlayer = {
+        let player = TSBusinessAudioPlayer()
+        player.loadLoactionURL(url: self.editOriginalURL)
+        player.currentTimeChangedHandle = { [weak self] current,total in
+            guard let self = self else { return }
+            handlePlayer(progressChanged: current, total: total)
+        }
+        
+        player.stateChangedHandle = { [weak self] state in
+            guard let self = self else { return }
+            handlePlayer(state: state)
+        }
+        
+        return player
+    }()
+    private lazy var audioTool = AudioTool()
+    private var maxVolume:Float{
+        return outputVolume * 0.5
+    }
+    
+    func reloadTrackView() {
+    
+        tempMp3Path = editOriginalURL.path
+
+        startCropRate = 0
+        endCropRate = 1.0
+        
+        DispatchQueue.main.async {
+            self.dragLeftView.centerX = self.trackContentView.x
+            self.dragRightView.centerX = self.trackContentView.frame.maxX
+            self.previousRightX = self.dragRightView.centerX
+            self.trackView.updateLeftCroppedPosition(1)
+        }
+
+        timeLabel.text = ringDuration.mmss
+    }
+
+
+    //#################################### UI 储存属性 ####################################//
+    lazy var fadeInDuration: Int = 0 {
+        didSet {
+            fadeinSliderView.rightLabel.text = "\(fadeInDuration)s"
+        }
+    }
+    lazy var fadeOutDuration: Int = 0 {
+        didSet {
+            fadeoutSliderView.rightLabel.text = "\(fadeOutDuration)s"
+        }
+    }
+    
+    lazy var outputVolume: Float = 1.0 {
+        didSet {
+            outputVolumeSliderView.rightLabel.text = "\(Int(outputVolume*100.0))%"
+        }
+    }
+    lazy var previousLeftX: CGFloat = 0
+    lazy var startCropRate: CGFloat = 0 {
+        didSet {
+            let time = startDuration
+            
+            //            print("---start: \(time)")
+            startTimeLabel.text = time.mmss
+            timeLabel.text = (endDuration - startDuration).mmss
+        }
+    }
+    // 拖拽时被暂停,拖拽结束后,继续播放
+    lazy var isSuspendByAction = false
+    lazy var endCropRate: CGFloat = 1.0 {
+        didSet {
+            let time = endDuration
+            endTimeLabel.text = time.mmss
+            timeLabel.text = (endDuration - startDuration).mmss
+        }
+    }
+    lazy var previousRightX: CGFloat = 0
+    
+    
+
+    override func startPlay() {
+        playButton.isSelected = !playButton.isSelected
+        if player.isPlaying {
+            player.pause()
+        } else {
+            player.playUrlString(ringModel.audioUrl,localURL: editOriginalURL)
+            player.playUrlString(ringModel.audioUrl,localURL: editOriginalURL)
+            self.player.seek(to:self.startDuration)
+            player.setVolume(volume: fadeInDuration > 0 ? 0 : maxVolume)
+
+            if UIApplication.getSystemVolume() == 0.0 {
+                TSToastShared.showToast(text:"Please turn up the volume".localized,duration: 2.0)
+            }
+            
+        }
+    }
+    
+    override func saveButtonClick() {
+        handleSaveCutAudio()
+    }
+    
+    override func setButtonClick() {
+        handleSaveCutAudio { model in
+            if let ringModel = model {
+                kDelayMainShort {
+                    let path = ringModel.documentPath.fillDocumentURL
+                    if let vc = WindowHelper.topViewController(){
+                        _ = kPurchaseToolShared.kshareBand(needVip: false, vc: vc, fileURL: path, fileName: ringModel.title)
+                    }
+                }
+            }
+        }
+    }
+    
+    override func navBarClickLeftAction() {
+        if isThereAnyChange(){
+            super.navBarClickLeftAction()
+        }else{
+            pop()
+        }
+    }
+    
+}
+
+
+extension TSEditAudioVC {
+    
+    @objc func sliderBeginTap(_ slider: UISlider) {
+        suspendPlay()
+    }
+    
+    @objc func sliderEndTap(_ slider: UISlider) {
+        autoPlayAfterMove()
+    }
+    
+    @objc func sliderValueChanged(_ slider: UISlider) {
+        //        print("---\(slider.value)")
+        if player.isPlaying {
+            player.pause()
+        }
+        if slider == fadeinSlider {
+            fadeInDuration = Int(slider.value.rounded(.toNearestOrEven))
+        } else if slider == fadeoutSlider {
+            fadeOutDuration = Int(slider.value.rounded(.toNearestOrEven))
+        } else if slider == outputVolumeSlider {
+            outputVolume = slider.value
+        }
+    }
+
+    // 裁剪起始时间
+    var startDuration: Double {
+        return startCropRate * CGFloat(ringDuration)
+    }
+    
+    var startMinCenterX: CGFloat {
+        return trackContentView.x
+    }
+    
+    var startMaxCenterX: CGFloat {
+        let ratio = (endCropRate * CGFloat(ringDuration) - 10) / CGFloat(ringDuration)
+        return ratio * trackContentView.width + trackContentView.x
+    }
+    
+    var endMinCenterX: CGFloat {
+        let ratio = (startCropRate * CGFloat(ringDuration) + 10) / CGFloat(ringDuration)
+        return ratio * trackContentView.width + trackContentView.x
+    }
+    
+    var endMaxCenterX: CGFloat {
+        return trackContentView.frame.maxX
+    }
+
+    @objc private func leftPanRecognizer(sender: UIPanGestureRecognizer) {
+        
+        dePrint("TSEditAudioVC leftPanRecognizer=\(sender)")
+        let limitMinCenterX: CGFloat = startMinCenterX
+        let limitMaxCenterX: CGFloat = startMaxCenterX
+        guard limitMaxCenterX > limitMinCenterX else {
+            if sender.state == .began {
+                dePrint("TSEditAudioVC rightPanRecognizer No less than 10s")
+                TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
+            }
+            return
+        }
+        
+        if sender.state == .began {
+            suspendPlay()
+        } else if sender.state == .changed {
+            // 修改位置
+            let newPoint = sender.translation(in: trackBgView)
+            var center = dragLeftView.center
+            center.x = previousLeftX + newPoint.x
+            guard center.x > limitMinCenterX,
+                  center.x < limitMaxCenterX else {
+                // 越界,拖不动了, 最少10s
+                if endDuration - startDuration < 11 {
+                    TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
+                }
+                return
+            }
+            dragLeftView.center = center
+            
+        } else if sender.state == .ended || sender.state == .failed {
+            previousLeftX = dragLeftView.center.x
+            autoPlayAfterMove()
+        }
+        
+        // 边界校验
+        if dragLeftView.centerX < limitMinCenterX {
+            dragLeftView.centerX = limitMinCenterX
+        }
+        if dragLeftView.centerX > limitMaxCenterX {
+            dragLeftView.centerX = limitMaxCenterX
+        }
+        
+        let position = dragLeftView.centerX - trackContentView.x
+        trackView.updateLeftCroppedPosition(position)
+        startCropRate = position / trackContentView.width
+        progressLine.centerX = dragLeftView.centerX
+    }
+    
+
+    var endDuration: Double {
+        return endCropRate * CGFloat(CGFloat(ringDuration))
+    }
+    
+
+    @objc private func rightPanRecognizer(sender: UIPanGestureRecognizer) {
+//        dePrint("TSEditAudioVC rightPanRecognizer=\(sender)")
+        let limitMinCenterX: CGFloat = endMinCenterX
+        let limitMaxCenterX: CGFloat = endMaxCenterX
+        
+
+        guard limitMaxCenterX > limitMinCenterX else {
+            if sender.state == .began {
+                dePrint("TSEditAudioVC rightPanRecognizer No less than 10s")
+                // 越界,拖不动了
+                TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
+            }
+            return
+        }
+        
+        if sender.state == .began {
+            suspendPlay()
+        } else if sender.state == .changed {
+            let newPoint = sender.translation(in: trackBgView)
+            //dePrint("rightPanRecognizer newPoint = \(newPoint)")
+            var center = dragRightView.center
+            //dePrint("rightPanRecognizer dragRightView.center = \(dragRightView.center)")
+            //dePrint("rightPanRecognizer previousRightX = \(previousRightX)")
+            center.x = previousRightX + newPoint.x
+            //dePrint("rightPanRecognizer center.x = \(center.x)")
+            //dePrint("rightPanRecognizer limitMinCenterX = \(limitMinCenterX),limitMaxCenterX = \(limitMaxCenterX)")
+            guard center.x > limitMinCenterX, center.x < limitMaxCenterX else {
+                // 越界,拖不动了, 最少10s
+                if endDuration - startDuration < 11 {
+                    TSToastShared.showToast(text:"No less than 10s".localized,duration: 2.0)
+                }
+                return
+            }
+            dragRightView.center = center
+            //dePrint("rightPanRecognizer center = \(center)")
+        } else if sender.state == .ended || sender.state == .failed {
+            previousRightX = dragRightView.centerX
+            autoPlayAfterMove()
+        }
+        
+        // 边界校验
+        if dragRightView.centerX > limitMaxCenterX {
+            dragRightView.centerX = limitMaxCenterX
+            //dePrint("rightPanRecognizer limitMaxCenterX = \(limitMaxCenterX)")
+        }
+        if dragRightView.centerX < limitMinCenterX {
+            dragRightView.centerX = limitMinCenterX
+            //dePrint("rightPanRecognizer limitMinCenterX = \(limitMinCenterX)")
+        }
+        
+        let position = dragRightView.centerX - trackContentView.x
+        //dePrint("rightPanRecognizer position = \(position)")
+        trackView.updateRightCroppedPosition(position)
+        endCropRate = position / trackContentView.width
+        //dePrint("rightPanRecognizer endCropRate = \(endCropRate)")
+    }
+    
+    // 拖拽,暂停
+    func suspendPlay() {
+        if player.isPlaying {
+            isSuspendByAction = true
+            player.pause()
+        }
+    }
+    
+    // 停止拖拽,继续播放
+    func autoPlayAfterMove() {
+        startPlay()
+        isSuspendByAction = false
+    }
+
+    
+    func handleSaveCutAudio(completion:((TSRingModel?)->Void)? = nil){
+        player.pause()
+        // 保存音频
+        startCutAudio { result, errMsg,savePath in
+            if let ringModel = result {
+                // 裁剪的音频,使用本地文件播放,清空网络url
+                dePrint("TSEditAudioVC saveModel ringModel = \(String(describing: ringModel.toJSONString()))")
+                TSMineRintoneHistory.shared.saveModel(model: ringModel)
+                
+                DispatchQueue.main.async {
+                    self.pop()
+                    if let window = WindowHelper.getKeyWindow() {
+                        kSaveSuccesswShared.show(atView:window,text: "Saved in “My Ringtone”".localized) {
+                            if let vc = WindowHelper.topViewController(){
+                                if vc is TSRingDownVC {
+                                    dePrint("vc 是 TSRingDownVC 类型")
+                                }else{
+                                    kPushVC(target: vc, modelVC: TSRingDownVC())
+                                }
+                            }
+                        }
+                    }
+                }
+                completion?(ringModel)
+            } else {
+                DispatchQueue.main.async {
+                    TSToastShared.showToast(text: errMsg ?? "Sorry, Save Failure".localized)
+                }
+                completion?(nil)
+            }
+        }
+    }
+
+    // 裁剪
+    func startCutAudio(completion: ((TSRingModel?, String?,URL?) -> Void)?) {
+        
+        let copyModel = ringModel
+        let savePath = TSDownloadManager.generateRingSaveLocalURL(name: copyModel.title)
+        TSFileManagerTool.checkFolderAndCreate(from: savePath)
+        
+        //未做任何改变,直接返回原音频
+        if isThereAnyChange() == false {
+            TSFileManagerTool.copyFileWithOverwrite(from: editOriginalURL, to: savePath)
+            copyModel.documentPath = savePath.path.documentLastURLString
+            //如果列表中有重名的,就加上时间戳
+            if let _ = TSMineRintoneHistory.shared.listModels.first(where: {$0.title == copyModel.title}){
+                copyModel.title = getModelNewTitle(title: copyModel.title)
+            }
+            completion?(copyModel, nil, savePath)
+            return
+        }
+        
+        copyModel.duration = Int(endDuration - startDuration)
+        TSRingLoadingView.shared.showWindow()
+        audioTool.startTansformAudio(url:editOriginalURL.path, from: startDuration, to: endDuration, fadeIn: Double(fadeInDuration), fadeOut: Double(fadeOutDuration),addVolume: Double(outputVolume) ,savePath: savePath.path) { [weak self] filePath, errMsg in
+            guard let self = self else { return }
+            DispatchQueue.main.async {
+                TSRingLoadingView.shared.remove()
+            }
+            if let filePath = filePath {
+                let url = URL(fileURLWithPath: filePath)
+                copyModel.documentPath = filePath.documentLastURLString
+                
+                if let fileInfo = TSBusinessAudioPlayer.getAudioFileInfo(path: url.path) {
+                    if let size = fileInfo.sizeInBytes {
+                        copyModel.size = Int(size)
+                    }
+                    if let duration = fileInfo.durationInSeconds {
+                        copyModel.duration = Int(duration)
+                    }
+                }
+                
+                //如果列表中有重名的,就加上时间戳
+                if let _ = TSMineRintoneHistory.shared.listModels.first(where: {$0.title == copyModel.title}){
+                    copyModel.title = self.getModelNewTitle(title: copyModel.title)
+                }
+
+                completion?(copyModel, errMsg, savePath)
+            } else {
+                completion?(nil, errMsg, nil)
+            }
+        }
+    }
+    
+    func getModelNewTitle(title:String)->String{
+        return title.removeTimestampFromEnd() + "_" + Date.timestampString
+    }
+    
+    //是否有改动,调整过编辑参数
+    func isThereAnyChange() -> Bool{
+        if startDuration == 0,
+           endDuration == ringDuration,
+           fadeInDuration == 0,
+           fadeOutDuration == 0,
+           outputVolume == 1.0{
+            return false
+        }
+           
+        return true
+    }
+}
+extension TSEditAudioVC {
+    func handlePlayer(state: TSBusinessAudioPlayer.PlayerState) {
+        playButton.isSelected = player.isPlaying
+        progressLine.isHidden = !playButton.isSelected
+        if player.currentPlayerState == .play {
+            progressLine.centerX = dragLeftView.centerX
+        }
+    }
+
+    func handlePlayer(progressChanged current: Double, total: Double) {
+        dePrint("TSEditAudioVC progressChanged=\(current),total=\(total)")
+        let range = endDuration - startDuration
+        let rangeWidth = trackContentView.width * range / total
+        let progress = max(0, current - startDuration) / range
+
+        if player.isPlaying {
+            progressLine.centerX = dragLeftView.centerX + progress * rangeWidth
+        }
+
+        if current >= endDuration {
+            player.pause()
+        }
+
+        // 淡入
+        if fadeInDuration > 0, current - startDuration < Double(fadeInDuration) {
+            let fadeProgress = Float(current - startDuration) / Float(fadeInDuration)
+            let newVolume = min(fadeProgress * maxVolume * 100, 100.0)
+            print("---volume: \(newVolume)")
+            player.setVolume(volume:newVolume / 100)
+        }
+
+        // 淡出
+        if fadeOutDuration > 0, current > (endDuration - Double(fadeOutDuration)) {
+            let fadeProgress = Float(endDuration - current) / Float(fadeOutDuration)
+            let newVolume = max(fadeProgress * maxVolume * 100, 0.0)
+            print("---volume: \(newVolume)")
+            player.setVolume(volume: newVolume / 10)
+        }
+    }
+
+    func sanitizeFilePath(_ path: String) -> String {
+        let illegalFileNameCharacters = CharacterSet(charactersIn: "/\\?%*|\"<>: ")
+        let sanitizedPath = path.components(separatedBy: illegalFileNameCharacters).joined(separator: "_")
+        return sanitizedPath
+    }
+}
+
+

+ 205 - 0
AIRingtone/Business/TSEditAudioVideoVC/TSEditAudioVideoBaseVC.swift

@@ -0,0 +1,205 @@
+//
+//  TSEditAudioVideoBaseVC.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/26.
+//
+import AVFoundation
+class TSEditAudioVideoBaseVC: TSBaseVC {
+    //#################################### 数据区域 ####################################//
+    var titleName:String{
+        "Edit".localized
+    }
+    var editOriginalURL:URL
+    
+    init(editOriginalURL:URL) {
+        self.editOriginalURL = editOriginalURL
+        super.init()
+    }
+    
+    @MainActor required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    //#################################### TSCustomStackView ####################################//
+    lazy var cusStackView: TSCustomStackView = {
+        let cusStackView = TSCustomStackView(axis: .vertical,spacing: 0)
+        return cusStackView
+    }()
+    
+
+    //#################################### play按钮 ####################################//
+    
+    lazy var playButtonView: UIView = {
+        let playButtonView = UIView(frame: CGRectMake(0, 0, k_ScreenWidth, 18+80+18))
+        
+        let playButtonBg = UIView()
+        playButtonBg.backgroundColor = .white.withAlphaComponent(0.1)
+        playButtonBg.cornerRadius = 40
+        playButtonView.addSubview(playButtonBg)
+        playButtonBg.snp.makeConstraints { make in
+            make.width.height.equalTo(80)
+            make.center.equalToSuperview()
+        }
+        
+        playButtonBg.addSubview(playButton)
+        playButton.snp.makeConstraints { make in
+            make.width.height.equalTo(60)
+            make.center.equalToSuperview()
+        }
+        
+        return playButtonView
+    }()
+    
+    lazy var playButton: UIButton = {//editaudio_play //editaudio_pause
+        let playButton = UIButton.createButton(image: UIImage(named: "editaudio_play")){ [weak self]  in
+            guard let self = self else { return }
+            startPlay()
+        }
+        playButton.setImage(UIImage(named: "editaudio_pause"), for: .selected)
+        return playButton
+    }()
+    
+    //#################################### 底部保存按钮等 ####################################//
+    lazy var cutButton: TSAppBtnView = { //保存
+        let cutButton = TSAppBtnView()
+        cutButton.setUpButton(style: .normalBorder, btnFrame: CGRectMake(0, 0, 98*kDesignScale, 48)) { [weak self]  in
+            guard let self = self else { return }
+            saveButtonClick()
+        }
+        cutButton.button.setTitle("Save".localized, for: .normal)
+        return cutButton
+    }()
+    
+    lazy var doneButton: TSAppBtnView = { // set
+        let doneButton = TSAppBtnView()
+        doneButton.setUpButton(style: .normalSet, btnFrame: CGRectMake(0, 0, 233*kDesignScale, 48)) { [weak self]  in
+            guard let self = self else { return }
+            setButtonClick()
+        }
+        doneButton.button.setTitle("Set Now".localized, for: .normal)
+        return doneButton
+    }()
+    
+    lazy var bottomBtnView: UIView = {
+        let bottomBtnView = UIView()
+        
+        bottomBtnView.addSubview(cutButton)
+        bottomBtnView.addSubview(doneButton)
+        
+        cutButton.snp.makeConstraints { make in
+            make.top.equalTo(16)
+            make.bottom.equalTo(-16)
+            make.leading.equalTo(16)
+            make.size.equalTo(cutButton.button.size)
+        }
+        
+        doneButton.snp.makeConstraints { make in
+            make.top.equalTo(16)
+            make.bottom.equalTo(-16)
+            make.trailing.equalTo(-16)
+            make.size.equalTo(doneButton.button.size)
+        }
+        return bottomBtnView
+    }()
+    
+    //#################################### viewDidLoad####################################//
+    override func createData() {
+
+    }
+    
+    override func createView() {
+        addNormalNavBarView()
+        setPageTitle(titleName)
+        
+        contentView.addSubview(bottomBtnView)
+        bottomBtnView.snp.makeConstraints { make in
+            make.leading.trailing.equalTo(0)
+            make.bottom.equalTo(-k_Height_safeAreaInsetsBottom())
+        }
+        
+        contentView.addSubview(cusStackView)
+        cusStackView.snp.makeConstraints { make in
+            make.top.leading.bottom.trailing.equalTo(0)
+            make.bottom.equalTo(bottomBtnView.snp.top)
+        }
+    }
+
+    lazy var operationCache: [TSRingModel] = []
+    var tempMp3Path: String?
+    var needClearTemp : Bool = false
+
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+
+        if needClearTemp {
+            deleteTempFiles()
+        }
+    }
+    
+    func deleteTempFiles() {
+        if let tempMp3Path = tempMp3Path {
+            let mp3Dir = NSString(string: tempMp3Path).deletingLastPathComponent
+            if FileManager.default.isDeletableFile(atPath: mp3Dir) {
+                let fileUrl = URL(fileURLWithPath: mp3Dir)
+                do {
+                    try FileManager.default.removeItem(at: fileUrl)
+                } catch {
+                    dePrint("TSEditAudioVideoBaseVC 删除文件失败")
+                }
+            }
+        }
+    }
+    
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        // 恢复右滑返回手势
+        navigationController?.interactivePopGestureRecognizer?.isEnabled = true
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        // 禁用右滑返回手势
+        navigationController?.interactivePopGestureRecognizer?.isEnabled = false
+
+    }
+    
+    deinit {
+        dePrint("RingEditViewController 销毁了")
+    }
+    
+    override func navBarClickLeftAction() {
+        TSCustomAlertController.show(in: self, config: TSCustomAlertController.AlertConfig(
+            message: "As you leave, the changes you have made will be lost.".localized,
+            messageColor: .white,
+            messageFont: .systemFont(ofSize: 16),
+            
+            cancelTitle: "Leave".localized,
+            cancelColor: .white,
+            
+            confirmTitle: "Stay".localized,
+            confirmColor: .themeColor,
+            
+            cancelAction: {
+                print("用户点击了Leave")
+                super.navBarClickLeftAction()
+            },
+            confirmAction: {
+                print("用户点击了Stay")
+            }
+        ))
+    }
+    
+    
+    
+    func startPlay() {
+        
+    }
+    
+    func saveButtonClick() {
+        
+    }
+    
+    func setButtonClick() {
+        
+    }
+}

+ 1 - 1
AIRingtone/Business/TSSetingVC/SetingVC/TSSetingVC.swift

@@ -92,7 +92,7 @@ class TSSetingVC: TSBaseVC {
             }
             
         }.store(in: &cancellable)
-        
+        vipInfoChanged()
         NotificationCenter.default.addObserver(self, selector: #selector(vipInfoChanged), name: .kPurchaseDidChanged, object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(refreshView), name: .kRefreshSettingView, object: nil)
     }

+ 12 - 3
AIRingtone/Business/TSThemeVC/TSThemeBrowseVC/TSThemeBrowseVC.swift

@@ -189,13 +189,22 @@ extension TSThemeBrowseVC {
     
     func setUpRingtone() {
         if let ringtone = currentModel?.ringtone {
-            TSCommonTool.downloadAndCacheFile(from: ringtone,missingEx: "mp3") { path, error in
-                if let path = path {
-                    self.currentRingtone = path
+            _ = TSDownloadManager.downloadFile(urlString: ringtone,missingEx: "mp3") {[weak self] url, error in
+                guard let self = self else { return }
+                if let path = url {
+                    self.currentRingtone = path.path
                 }else{
                     self.currentRingtone = nil
                 }
+                
             }
+//            TSCommonTool.downloadAndCacheFile(from: ringtone) { path, error in
+//                if let path = path {
+//                    self.currentRingtone = path
+//                }else{
+//                    self.currentRingtone = nil
+//                }
+//            }
         }else{
             self.currentRingtone = nil
         }

+ 9 - 3
AIRingtone/Business/TSThemeVC/TSThemeSetVC/TSThemeSetVC.swift

@@ -143,7 +143,13 @@ extension TSThemeSetVC {
     @objc func clickSetRing(){
         audioPlayer.stop()
 //        if kPurchaseToolShared.kJudgeVip(externalBool: getNeedVip, vc: self){ return }//判断 vip
-        _ = kPurchaseToolShared.kshareBand(needVip: getNeedVip, vc: self, urlString: model.ringtone, fileName:  model.name)
+        
+        TSDownloadManager.getDownLoadRing(urlString: model.ringtone){ [weak self] url,downLoad in
+            guard let self = self else { return }
+            if let path = url {
+                _ = kPurchaseToolShared.kshareBand(needVip:getNeedVip, vc: self, fileURL: path, fileName: model.ringtoneName)
+            }
+        }
         
     }
     
@@ -154,7 +160,7 @@ extension TSThemeSetVC {
         if let image = posterView.netWorkImageView.image{
             PhotoManagerShared.saveImageToAlbum(image) { success, error in
                 if success {
-                    kSavePhotoSuccesswShared.show(atView: self.view)
+                    kSaveSuccesswShared.show(atView: self.view)
                 }else{
                     debugPrint(error)
                 }
@@ -171,7 +177,7 @@ extension TSThemeSetVC {
         guard let photo = avatarImage else { return }
         contactsTool.setContactsAvatar(avatarImage: photo) { data, error in
             if error == nil {
-                kSavePhotoSuccesswShared.show(atView: self.view,showViewBtn: false)
+                kSaveSuccesswShared.show(atView: self.view,showViewBtn: false)
             }
         }
     }

+ 1 - 1
AIRingtone/Business/TSThemeVC/TSThemeVC/Model/TSThemeModel.swift

@@ -14,7 +14,7 @@ class TSThemeModel: TSBaseModel {
     var poster:String = ""//海报
     var themeId:String = ""
     var ringtone:String = ""//铃声
-    var ringtoneName:String = ""//铃声名
+    var ringtoneName:String = ""//铃声名
     var ringtoneCover:String = ""//铃声封面
     
     

+ 50 - 8
AIRingtone/Business/VIewTool/TSButton.swift

@@ -19,7 +19,6 @@ class TSNormalSubmitBtn: TSAppBtn {
     }
 }
 
-
 class TSNormalCancelBtn: TSAppBtn {
 
 }
@@ -41,7 +40,9 @@ class TSAppBtnView: TSBaseView {
         case normal
         case generate   //创造类的按钮
         case themeSet   //主题页设置按钮
-        
+    
+        case normalSet   //普通的设置按钮
+        case normalBorder   //普通的边框
     }
     
     var viewH:CGFloat = 64
@@ -52,7 +53,7 @@ class TSAppBtnView: TSBaseView {
         }
     }
     var clickBlock:(()->Void)?
-    
+    var btnFrame:CGRect?
     //###################################### Button ######################################
     var button:UIButton = UIButton()
 
@@ -64,8 +65,8 @@ class TSAppBtnView: TSBaseView {
         
     }
 
-    func setUpButton(style:ViewStyle,vipFreeNumType:VipFreeNumType,clickBlock: @escaping () -> Void) {
-        
+    func setUpButton(style:ViewStyle,vipFreeNumType:VipFreeNumType = .none,btnFrame:CGRect? = nil,clickBlock: @escaping () -> Void) {
+        self.btnFrame = btnFrame
         self.style = style
         self.vipFreeNumType = vipFreeNumType
         self.clickBlock = clickBlock
@@ -78,6 +79,10 @@ class TSAppBtnView: TSBaseView {
             launchVipLogic()
         case .themeSet:
             setUpThemeSet()
+        case .normalSet:
+            setUpNormalSet()
+        case .normalBorder:
+            setUpNormalBorder()
         default:
             break
         }
@@ -156,10 +161,26 @@ extension TSAppBtnView{
     }
     
     func setUpThemeSet() {
-        button = kCreateNormalSubmitBtn(title: "Set Now".localized, frame: CGRectMake(0, 0, 200, 48),action: { [weak self]  in
+        button = kCreateNormalSubmitBtn(title: "Set Now".localized, frame: self.btnFrame ?? CGRectMake(0, 0, 200, 48),action: { [weak self]  in
+            guard let self = self else { return }
+            clickBlock?()
+        })
+        contentView.addSubview(button)
+        button.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.equalTo(button.width)
+            make.height.equalTo(button.height)
+        }
+   
+    }
+    
+    func setUpNormalSet() {
+        let frame = self.btnFrame ?? CGRectMake(0, 0, 233, 48)
+        button = kCreateNormalSubmitBtn(title: "Set Now".localized, frame: frame,action: { [weak self]  in
             guard let self = self else { return }
             clickBlock?()
         })
+        button.titleLabel?.numberOfLines  = 3
         contentView.addSubview(button)
         button.snp.makeConstraints { make in
             make.center.equalToSuperview()
@@ -168,6 +189,27 @@ extension TSAppBtnView{
         }
    
     }
+    
+    func setUpNormalBorder() {
+        
+        let textColor = UIColor.themeColor
+        let frame = self.btnFrame ?? CGRectMake(0, 0, 98, 48)
+        button = UIButton.createButton(backgroundColor: "#171717".uiColor,font: UIFont.font(size: 16,weight: .regular),titleColor:textColor,corner: frame.size.height/2){ [weak self]  in
+            guard let self = self else { return }
+            clickBlock?()
+        }
+        button.frame = frame
+        button.layer.borderWidth = 1
+        button.layer.borderColor = textColor.cgColor
+
+
+        contentView.addSubview(button)
+        button.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.equalTo(button.width)
+            make.height.equalTo(button.height)
+        }
+    }
 }
 //创造按钮
 extension TSAppBtnView{
@@ -186,9 +228,9 @@ extension TSAppBtnView{
 //常用提交按钮
 func kCreateNormalSubmitBtn(title:String,frame:CGRect,action: (() -> Void)? = nil) -> UIButton {
     let btn = TSNormalSubmitBtn()
-    btn.setUpButton(title:title,font: UIFont.font(size: 16,weight: .regular),titleColor:.white,corner: 24,action: action)
-    btn.setTitleImageSpace(spacing: 4)
     btn.frame = frame
+    btn.setUpButton(title:title,font: UIFont.font(size: 16,weight: .regular),titleColor:.white,corner: frame.height/2,action: action)
+    btn.setTitleImageSpace(spacing: 4)
     btn.addGradientBg(colors: ["#E961F6".uiColor.cgColor,"#7E57F4".uiColor.cgColor])
     return btn
 }

+ 13 - 0
AIRingtone/Business/VIewTool/TSRingLoadingView.swift

@@ -9,6 +9,19 @@ import Kingfisher
 
 class TSRingLoadingView: TSBaseView {
     
+    static let shared = TSRingLoadingView(frame: CGRectMake(0, 0, k_ScreenWidth, k_ScreenHeight))
+    
+    func showWindow(){
+        if let window = WindowHelper.getKeyWindow() {
+            window.addSubview(TSRingLoadingView.shared)
+            TSRingLoadingView.shared.isRotating = true
+        }
+    }
+    
+    func remove(){
+        TSRingLoadingView.shared.removeFromSuperview()
+    }
+    
     lazy var animatedImageView: AnimatedImageView = {
         let animatedImageView = AnimatedImageView()
         animatedImageView.autoPlayAnimatedImage = false

+ 39 - 3
AIRingtone/Business/VIewTool/TSRingToneCellView.swift

@@ -26,7 +26,29 @@ class TSRingToneCellView: TSBaseView {
     let nameLab = UILabel.createLabel(text: "--",font: .font(size: 14),textColor: .white)
     let timeLab = UILabel.createLabel(text:"--:--",font: .font(size: 12),textColor: .white.withAlphaComponent(0.6))
     
-
+ 
+    var clickSetUpHandel:(()->Void)?
+    lazy var setRingBtn: TSUIExpandedTouchButton = {
+        let setRingBtn = TSUIExpandedTouchButton()
+        setRingBtn.setUpButton(image: UIImage(named: "ai_setRing_icon")){ [weak self]  in
+            guard let self = self else { return }
+            clickSetUpHandel?()
+        }
+        setRingBtn.isHidden = true
+        return setRingBtn
+    }()
+    
+    var clickEditHandel:(()->Void)?
+    lazy var editBtn: TSUIExpandedTouchButton = {
+        let editBtn = TSUIExpandedTouchButton()
+        editBtn.setUpButton(image: UIImage(named: "edit_ring_icon")){ [weak self]  in
+            guard let self = self else { return }
+            clickEditHandel?()
+        }
+        editBtn.isHidden = true
+        return editBtn
+    }()
+    
     lazy var clearBtn = UIButton.createButton{[weak self]  in
         guard let self = self else { return }
         clickPlayHandel?(self.isPlay)
@@ -87,7 +109,7 @@ class TSRingToneCellView: TSBaseView {
 
         nameLab.snp.makeConstraints { make in
             make.leading.equalTo(76)
-            make.trailing.equalTo(-60)
+            make.trailing.equalTo(-86)
             make.top.equalTo(17)
         }
         
@@ -110,6 +132,20 @@ class TSRingToneCellView: TSBaseView {
             make.leading.top.bottom.equalToSuperview()
             make.trailing.equalTo(0)
         }
+        
+        contentView.addSubview(setRingBtn)
+        setRingBtn.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalTo(-16)
+            make.width.height.equalTo(20)
+        }
+        
+        contentView.addSubview(editBtn)
+        editBtn.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.trailing.equalTo(setRingBtn.snp.leading).offset(-20)
+            make.width.height.equalTo(20)
+        }
     }
     
     
@@ -163,7 +199,7 @@ class TSRingToneGenerateView:TSBaseView {
     
     
     var refreshHandel:(()->Void)?
-    
+
     lazy var generateProgressView: UIImageView = {
         let generateProgressView = UIImageView.createImageView(imageName: "ringGenerateProgress",contentMode: .scaleToFill)
         return generateProgressView

+ 18 - 11
AIRingtone/Business/VIewTool/TSSavePhotoSuccessTool.swift

@@ -1,14 +1,16 @@
 //
-//  TSSavePhotoSuccessTool.swift
+//  TSSaveSuccessTool.swift
 //  AIRingtone
 //
 //  Created by 100Years on 2025/3/5.
 //
 
-let kSavePhotoSuccesswShared = TSSavePhotoSuccessTool.shared
-class TSSavePhotoSuccessTool {
+let kSaveSuccesswShared = TSSaveSuccessTool.shared
+class TSSaveSuccessTool {
     
-    static let shared = TSSavePhotoSuccessTool()
+    static let shared = TSSaveSuccessTool()
+    
+    var clickViewHandle:(()->Void)?
     
     private lazy var textLabel:UILabel = {
         let textLabel = UILabel()
@@ -25,11 +27,16 @@ class TSSavePhotoSuccessTool {
     
     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) {
-            if let url = URL(string: "photos-redirect://") {
-                if UIApplication.shared.canOpenURL(url) {
-                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
-                    playVibration()
+        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()
+                    }
                 }
             }
         }
@@ -86,8 +93,8 @@ class TSSavePhotoSuccessTool {
 
     
     
-    func show(atView:UIView,text:String = "Save Successfully".localized,showViewBtn:Bool = true) {
-        
+    func show(atView:UIView,text:String = "Save Successfully".localized,showViewBtn:Bool = true,clickViewHandle:(()->Void)? = nil) {
+        self.clickViewHandle = clickViewHandle
         kExecuteOnMainThread {
             self.textLabel.text = text
             self.viewButton.isHidden = !showViewBtn

+ 31 - 34
AIRingtone/Common/NetworkManager/TSNetWork/TSNetWork+Business.swift

@@ -83,40 +83,8 @@ extension TSNetworkManager {
     func removeCache(urlType: TSNeURLType) {
         removeCache(urlString: urlType.getUrlString())
     }
-//    /// 通用 POST Stream 请求
-//    /// - Parameters:
-//    ///   - endpoint: 接口路径
-//    ///   - parameters: 请求参数
-//    ///   - responseType: 响应数据模型(可选)
-//    ///   - completion: 请求完成的回调
-//    func postStream<T: TSBaseModel>(
-//        urlType: TSNeURLType,
-//        parameters: [String: Any]? = nil,
-//        responseType: T.Type? = nil,
-//        streamHandler:@escaping (String) -> Void,
-//        completion: @escaping (Result<Any, Error>) -> Void
-//    ) -> StreamPostRequest{
-//        let urlString = urlType.getUrlString()
-//        
-//        let streamRequest = StreamPostRequest(url: URL(string: urlString)!, parameters: parameters) { data in
-//            if let string = String(data: data, encoding: .utf8) {
-//                print("Received chunk: \(string)")
-//                streamHandler(string)
-//            }
-//        } completionHandler: { error in
-//            if let error = error {
-//                print("Request failed with error: \(error)")
-//                completion(.failure(error))
-//            } else {
-//                print("Request completed successfully.")
-//                completion(.success("success"))
-//            }
-//        }
-//        streamRequest.startRequest()
-//        return streamRequest
-//    }
-    
-    
+
+
     /// 通用 POST Stream 请求
     /// - Parameters:
     ///   - endpoint: 接口路径
@@ -162,6 +130,35 @@ extension TSNetworkManager {
         },completion: completion)
         return request
     }
+    
+    
+    func downloadFile(
+        urlString: String,
+        to destination: URL,
+        progressHandler: ((Double) -> Void)? = nil,
+        completion: @escaping (URL?, Error?) -> Void
+    ) -> DownloadRequest? {
+        let request = self.downloadFile(
+            urlString: urlString,
+            to: destination,
+            progressHandler: { progress in
+                print("下载进度: \(progress * 100)%")
+                progressHandler?(progress)
+            },
+            completion: { result in
+                switch result {
+                case .success(let fileURL):
+                    dePrint("下载完成,文件保存在: \(fileURL.path)")
+                    completion(fileURL,nil)
+                case .failure(let error):
+                    dePrint("下载失败: \(error.localizedDescription)")
+                    completion(nil,error)
+                }
+            }
+        )
+        return request
+    }
+    
 
 }
 

+ 65 - 0
AIRingtone/Common/NetworkManager/TSNetWork/TSNetworkManager.swift

@@ -306,6 +306,71 @@ extension TSNetworkManager {
         
         return request
     }
+    
+    
+    /// 下载文件
+    /// - Parameters:
+    ///   - url: 下载URL
+    ///   - destination: 目标保存路径 (可选,不传则使用临时目录)
+    ///   - progressHandler: 进度回调 (0.0~1.0)
+    ///   - completion: 完成回调 (返回文件URL或错误)
+    func downloadFile(
+        urlString: String,
+        to destination: URL? = nil,
+        progressHandler: ((Double) -> Void)? = nil,
+        completion: @escaping (Result<URL, Error>) -> Void
+    ) -> DownloadRequest? {
+        
+        
+        guard let url = URL(string: urlString) else {
+            completion(.failure(NSError(domain: "url nil", code: 0)))
+            return nil
+        }
+        
+        // 设置下载目标路径
+        let destination: DownloadRequest.Destination = { temporaryURL, response in
+            // 如果用户指定了目标路径
+            if let destination = destination {
+                // 确保目录存在
+                let directory = destination.deletingLastPathComponent()
+                try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
+                return (destination, [.removePreviousFile, .createIntermediateDirectories])
+            }
+            
+            // 否则使用临时目录
+            let documentsURL = FileManager.default.temporaryDirectory
+            let suggestedFilename = response.suggestedFilename ?? url.lastPathComponent
+            let fileURL = documentsURL.appendingPathComponent(suggestedFilename)
+            
+            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
+        }
+        
+        // 开始下载
+        let request = AF.download(url, to: destination)
+            .downloadProgress { progress in
+                // 主线程回调进度
+                DispatchQueue.main.async {
+                    progressHandler?(progress.fractionCompleted)
+                }
+            }
+            .response { response in
+                // 主线程回调结果
+                DispatchQueue.main.async {
+                    switch response.result {
+                    case .success(let fileURL):
+                        if let fileURL = fileURL {
+                            completion(.success(fileURL))
+                        } else {
+                            completion(.failure(NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "文件路径无效"])))
+                        }
+                    case .failure(let error):
+                        completion(.failure(error))
+                    }
+                }
+            }
+        
+        return request
+    }
 
 }
 

+ 2 - 2
AIRingtone/Common/Purchase/TSPurchaseManager/TSPurchaseTool.swift

@@ -143,7 +143,7 @@ extension TSPurchaseTool{
     
     func kshareBand(needVip:Bool,
                    vc:UIViewController,
-                    urlString:String,
+                    fileURL:URL,
                     fileName:String,
                     completion:((Bool)->Void)? = nil) -> Bool {
         //判断 vip
@@ -153,7 +153,7 @@ extension TSPurchaseTool{
             TSPurchaseVC.show(target: vc, closePageBlock: nil)
             return true
         }
-        TSBandRingTool.creatBandRingTool().shareBandVC(vc: vc, fileURLString: urlString, fileName: fileName,completion: completion)
+        TSBandRingTool.creatBandRingTool().shareBandVC(vc: vc, fileURL: fileURL, fileName: fileName,completion: completion)
         return false
     }
 }

+ 56 - 56
AIRingtone/Common/Tool/NotUse/TSAudioPlayerFileTool.swift

@@ -5,59 +5,59 @@
 //  Created by 100Years on 2025/3/18.
 //
 
-class TSAudioPlayerFileTool {
-    
-    
-    var downloadOp:DownloadOperation?
-    var urlString:String
-    init(urlString:String) {
-        self.urlString = urlString
-
-    }
-    
-    
-    func playAudio(urlString:String){
-        guard let url = URL(string: urlString) else { return }
-        
-        // 检查本地是否已存在文件
-        if let localFilePath = DownloadManager.localFilePath(for: url) {
-            print("File already exists at: \(localFilePath)")
-            kAudioAVPlayerShared.loadAudio(from: localFilePath)
-            kAudioAVPlayerShared.play()
-        } else {
-            // 开始下载任务
-            downloadOp = DownloadManager.shared.downloadFile(from: url)
-
-            // 设置进度回调
-            downloadOp?.progressHandler = { progress in
-                print("Download progress: \(progress)")
-                
-                kAudioAVPlayerShared.onPlaybackStateChanged?(.loading(progress))
-            }
-
-            // 设置完成回调
-            downloadOp?.completionHandler = { tempURL, error in
-                if let tempURL = tempURL {
-                    print("Download completed, file saved at: \(tempURL)")
-                    kAudioAVPlayerShared.loadAudio(from: tempURL)
-                    kAudioAVPlayerShared.play()
-                    
-                } else if let error = error {
-                    print("Download failed: \(error)")
-                }
-            }
-
-            // 开始下载
-            downloadOp?.startDownload()
-        }
-    }
-    
-    func stopAudio(urlString:String){
-        
-        if let downloadOp = downloadOp {
-            // 暂停下载
-            downloadOp.pause()
-            kAudioAVPlayerShared.stop()
-        }
-    }
-}
+//class TSAudioPlayerFileTool {
+//    
+//    
+//    var downloadOp:DownloadOperation?
+//    var urlString:String
+//    init(urlString:String) {
+//        self.urlString = urlString
+//
+//    }
+//    
+//    
+//    func playAudio(urlString:String){
+//        guard let url = URL(string: urlString) else { return }
+//        
+//        // 检查本地是否已存在文件
+//        if let localFilePath = DownloadManager.localFilePath(for: url) {
+//            print("File already exists at: \(localFilePath)")
+//            kAudioAVPlayerShared.loadAudio(from: localFilePath)
+//            kAudioAVPlayerShared.play()
+//        } else {
+//            // 开始下载任务
+//            downloadOp = DownloadManager.shared.downloadFile(from: url)
+//
+//            // 设置进度回调
+//            downloadOp?.progressHandler = { progress in
+//                print("Download progress: \(progress)")
+//                
+//                kAudioAVPlayerShared.onPlaybackStateChanged?(.loading(progress))
+//            }
+//
+//            // 设置完成回调
+//            downloadOp?.completionHandler = { tempURL, error in
+//                if let tempURL = tempURL {
+//                    print("Download completed, file saved at: \(tempURL)")
+//                    kAudioAVPlayerShared.loadAudio(from: tempURL)
+//                    kAudioAVPlayerShared.play()
+//                    
+//                } else if let error = error {
+//                    print("Download failed: \(error)")
+//                }
+//            }
+//
+//            // 开始下载
+//            downloadOp?.startDownload()
+//        }
+//    }
+//    
+//    func stopAudio(urlString:String){
+//        
+//        if let downloadOp = downloadOp {
+//            // 暂停下载
+//            downloadOp.pause()
+//            kAudioAVPlayerShared.stop()
+//        }
+//    }
+//}

+ 0 - 214
AIRingtone/Common/Tool/NotUse/TSDownloadManager.swift

@@ -1,214 +0,0 @@
-//
-//  TSDownloadManager.swift
-//  AIRingtone
-//
-//  Created by 100Years on 2025/3/18.
-//
-import Foundation
-
-// MARK: - DownloadManager
-/// 下载管理器,负责生成和管理下载任务
-class DownloadManager {
-    static let shared = DownloadManager()
-    
-    private var maxConcurrentDownloads: Int
-    private let downloadQueue: OperationQueue
-    private var activeDownloads: [URL: DownloadOperation] = [:]
-    private var expirationInterval: TimeInterval = 7 * 24 * 60 * 60 // 默认 7 天
-    
-    private init() {
-        self.maxConcurrentDownloads = 3 // 默认最大并发下载数为 3
-        self.downloadQueue = OperationQueue()
-        self.downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads
-    }
-    
-    /// 设置最大并发下载数
-    /// - Parameter count: 最大并发下载数
-    func setMaxConcurrentDownloads(_ count: Int) {
-        maxConcurrentDownloads = count
-        downloadQueue.maxConcurrentOperationCount = count
-    }
-    
-    /// 设置缓存文件的过期时长
-    /// - Parameter interval: 过期时长(秒)
-    func setExpirationInterval(_ interval: TimeInterval) {
-        expirationInterval = interval
-    }
-    
-    /// 创建下载任务
-    /// - Parameter url: 下载文件的 URL
-    /// - Returns: 返回 DownloadOperation,用于控制下载任务
-    func downloadFile(from url: URL) -> DownloadOperation {
-        if let downloadOperation = activeDownloads[url] {
-            return downloadOperation
-        }
-        
-        let downloadOperation = DownloadOperation(url: url)
-        activeDownloads[url] = downloadOperation
-        downloadQueue.addOperation(downloadOperation)
-        return downloadOperation
-    }
-    
-    /// 清理过期的缓存文件
-    func cleanExpiredFiles() {
-        let fileManager = FileManager.default
-        let cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
-        let expirationDate = Date().addingTimeInterval(-expirationInterval)
-        
-        if let files = try? fileManager.contentsOfDirectory(at: cacheDirectory, includingPropertiesForKeys: [.creationDateKey], options: .skipsHiddenFiles) {
-            for file in files {
-                if let attributes = try? fileManager.attributesOfItem(atPath: file.path),
-                   let creationDate = attributes[.creationDate] as? Date,
-                   creationDate < expirationDate {
-                    try? fileManager.removeItem(at: file)
-                }
-            }
-        }
-    }
-    
-    /// 获取本地文件路径(如果存在)
-    /// - Parameter url: 文件的 URL
-    /// - Returns: 本地文件路径(如果存在),否则返回 nil
-    static func localFilePath(for url: URL) -> URL? {
-        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-        let fileName = url.lastPathComponent
-        let fileURL = cacheDirectory.appendingPathComponent(fileName)
-        if FileManager.default.fileExists(atPath: fileURL.path) {
-            return fileURL
-        }
-        return nil
-    }
-    
-    /// 清理所有下载任务
-    func cleanAllTasks() {
-        for (url, downloadOperation) in activeDownloads {
-            downloadOperation.cancel()
-            activeDownloads[url] = nil
-        }
-    }
-    
-    /// 清理某个下载任务
-    /// - Parameter url: 下载任务的 URL
-    func cleanTask(for url: URL) {
-        if let downloadOperation = activeDownloads[url] {
-            downloadOperation.cancel()
-            activeDownloads[url] = nil
-        }
-    }
-}
-
-// MARK: - DownloadOperation
-/// 下载任务,继承自 Operation
-class DownloadOperation: Operation {
-    private let url: URL
-    private var downloadTask: URLSessionDownloadTask?
-    private var resumeData: Data?
-    private var isPaused: Bool = false
-    
-    /// 下载进度回调
-    var progressHandler: ((Double) -> Void)?
-    
-    /// 下载完成回调
-    var completionHandler: ((URL?, Error?) -> Void)?
-    
-    init(url: URL) {
-        self.url = url
-    }
-    
-    /// 开始下载
-    func startDownload() {
-        // 检查本地是否已存在文件
-        if let localFilePath = DownloadManager.localFilePath(for: url) {
-            completionHandler?(localFilePath, nil)
-            return
-        }
-        
-        // 如果没有本地文件,则开始下载
-        let semaphore = DispatchSemaphore(value: 0)
-        
-        // 检查是否有断点续传数据
-        if let resumeData = loadResumeData() {
-            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
-            downloadTask = session.downloadTask(withResumeData: resumeData)
-        } else {
-            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
-            downloadTask = session.downloadTask(with: url)
-        }
-        
-        downloadTask?.resume()
-        semaphore.wait()
-    }
-    
-    /// 暂停下载
-    func pause() {
-        isPaused = true
-        downloadTask?.cancel { [weak self] resumeData in
-            if let resumeData = resumeData {
-                self?.saveResumeData(resumeData)
-            }
-        }
-    }
-    
-    /// 恢复下载
-    func resume() {
-        isPaused = false
-        if let resumeData = loadResumeData() {
-            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
-            downloadTask = session.downloadTask(withResumeData: resumeData)
-            downloadTask?.resume()
-        }
-    }
-    
-    /// 取消下载
-    override func cancel() {
-        downloadTask?.cancel()
-        super.cancel()
-    }
-    
-    /// 保存断点续传数据到文件
-    private func saveResumeData(_ data: Data) {
-        let resumeDataURL = getResumeDataURL()
-        try? data.write(to: resumeDataURL)
-    }
-    
-    /// 从文件加载断点续传数据
-    private func loadResumeData() -> Data? {
-        let resumeDataURL = getResumeDataURL()
-        return try? Data(contentsOf: resumeDataURL)
-    }
-    
-    /// 获取断点续传数据的存储路径
-    private func getResumeDataURL() -> URL {
-        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-        let resumeDataFileName = url.lastPathComponent + ".resumeData"
-        return cacheDirectory.appendingPathComponent(resumeDataFileName)
-    }
-}
-
-// MARK: - URLSessionDownloadDelegate
-extension DownloadOperation: URLSessionDownloadDelegate {
-    /// 下载进度更新
-    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
-        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
-        progressHandler?(progress)
-    }
-    
-    /// 下载完成
-    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
-        if !isPaused {
-            // 将文件移动到缓存目录
-            let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-            let fileName = url.lastPathComponent
-            let destinationURL = cacheDirectory.appendingPathComponent(fileName)
-            try? FileManager.default.moveItem(at: location, to: destinationURL)
-            completionHandler?(destinationURL, nil)
-        }
-    }
-    
-    /// 任务完成(包括成功或失败)
-    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
-        if let error = error {
-            completionHandler?(nil, error)
-        }
-    }
-}

+ 214 - 0
AIRingtone/Common/Tool/NotUse/TSDownloadTool.swift

@@ -0,0 +1,214 @@
+//
+//  TSDownloadTool.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/18.
+//
+import Foundation
+
+//// MARK: - TSDownloadTool
+///// 下载管理器,负责生成和管理下载任务
+//class TSDownloadTool {
+//    static let shared = TSDownloadTool()
+//    
+//    private var maxConcurrentDownloads: Int
+//    private let downloadQueue: OperationQueue
+//    private var activeDownloads: [URL: DownloadOperation] = [:]
+//    private var expirationInterval: TimeInterval = 7 * 24 * 60 * 60 // 默认 7 天
+//    
+//    private init() {
+//        self.maxConcurrentDownloads = 3 // 默认最大并发下载数为 3
+//        self.downloadQueue = OperationQueue()
+//        self.downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads
+//    }
+//    
+//    /// 设置最大并发下载数
+//    /// - Parameter count: 最大并发下载数
+//    func setMaxConcurrentDownloads(_ count: Int) {
+//        maxConcurrentDownloads = count
+//        downloadQueue.maxConcurrentOperationCount = count
+//    }
+//    
+//    /// 设置缓存文件的过期时长
+//    /// - Parameter interval: 过期时长(秒)
+//    func setExpirationInterval(_ interval: TimeInterval) {
+//        expirationInterval = interval
+//    }
+//    
+//    /// 创建下载任务
+//    /// - Parameter url: 下载文件的 URL
+//    /// - Returns: 返回 DownloadOperation,用于控制下载任务
+//    func downloadFile(from url: URL) -> DownloadOperation {
+//        if let downloadOperation = activeDownloads[url] {
+//            return downloadOperation
+//        }
+//        
+//        let downloadOperation = DownloadOperation(url: url)
+//        activeDownloads[url] = downloadOperation
+//        downloadQueue.addOperation(downloadOperation)
+//        return downloadOperation
+//    }
+//    
+//    /// 清理过期的缓存文件
+//    func cleanExpiredFiles() {
+//        let fileManager = FileManager.default
+//        let cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
+//        let expirationDate = Date().addingTimeInterval(-expirationInterval)
+//        
+//        if let files = try? fileManager.contentsOfDirectory(at: cacheDirectory, includingPropertiesForKeys: [.creationDateKey], options: .skipsHiddenFiles) {
+//            for file in files {
+//                if let attributes = try? fileManager.attributesOfItem(atPath: file.path),
+//                   let creationDate = attributes[.creationDate] as? Date,
+//                   creationDate < expirationDate {
+//                    try? fileManager.removeItem(at: file)
+//                }
+//            }
+//        }
+//    }
+//    
+//    /// 获取本地文件路径(如果存在)
+//    /// - Parameter url: 文件的 URL
+//    /// - Returns: 本地文件路径(如果存在),否则返回 nil
+//    static func localFilePath(for url: URL) -> URL? {
+//        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+//        let fileName = url.lastPathComponent
+//        let fileURL = cacheDirectory.appendingPathComponent(fileName)
+//        if FileManager.default.fileExists(atPath: fileURL.path) {
+//            return fileURL
+//        }
+//        return nil
+//    }
+//    
+//    /// 清理所有下载任务
+//    func cleanAllTasks() {
+//        for (url, downloadOperation) in activeDownloads {
+//            downloadOperation.cancel()
+//            activeDownloads[url] = nil
+//        }
+//    }
+//    
+//    /// 清理某个下载任务
+//    /// - Parameter url: 下载任务的 URL
+//    func cleanTask(for url: URL) {
+//        if let downloadOperation = activeDownloads[url] {
+//            downloadOperation.cancel()
+//            activeDownloads[url] = nil
+//        }
+//    }
+//}
+//
+//// MARK: - DownloadOperation
+///// 下载任务,继承自 Operation
+//class DownloadOperation: Operation {
+//    private let url: URL
+//    private var downloadTask: URLSessionDownloadTask?
+//    private var resumeData: Data?
+//    private var isPaused: Bool = false
+//    
+//    /// 下载进度回调
+//    var progressHandler: ((Double) -> Void)?
+//    
+//    /// 下载完成回调
+//    var completionHandler: ((URL?, Error?) -> Void)?
+//    
+//    init(url: URL) {
+//        self.url = url
+//    }
+//    
+//    /// 开始下载
+//    func startDownload() {
+//        // 检查本地是否已存在文件
+//        if let localFilePath = TSDownloadTool.localFilePath(for: url) {
+//            completionHandler?(localFilePath, nil)
+//            return
+//        }
+//        
+//        // 如果没有本地文件,则开始下载
+//        let semaphore = DispatchSemaphore(value: 0)
+//        
+//        // 检查是否有断点续传数据
+//        if let resumeData = loadResumeData() {
+//            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
+//            downloadTask = session.downloadTask(withResumeData: resumeData)
+//        } else {
+//            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
+//            downloadTask = session.downloadTask(with: url)
+//        }
+//        
+//        downloadTask?.resume()
+//        semaphore.wait()
+//    }
+//    
+//    /// 暂停下载
+//    func pause() {
+//        isPaused = true
+//        downloadTask?.cancel { [weak self] resumeData in
+//            if let resumeData = resumeData {
+//                self?.saveResumeData(resumeData)
+//            }
+//        }
+//    }
+//    
+//    /// 恢复下载
+//    func resume() {
+//        isPaused = false
+//        if let resumeData = loadResumeData() {
+//            let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
+//            downloadTask = session.downloadTask(withResumeData: resumeData)
+//            downloadTask?.resume()
+//        }
+//    }
+//    
+//    /// 取消下载
+//    override func cancel() {
+//        downloadTask?.cancel()
+//        super.cancel()
+//    }
+//    
+//    /// 保存断点续传数据到文件
+//    private func saveResumeData(_ data: Data) {
+//        let resumeDataURL = getResumeDataURL()
+//        try? data.write(to: resumeDataURL)
+//    }
+//    
+//    /// 从文件加载断点续传数据
+//    private func loadResumeData() -> Data? {
+//        let resumeDataURL = getResumeDataURL()
+//        return try? Data(contentsOf: resumeDataURL)
+//    }
+//    
+//    /// 获取断点续传数据的存储路径
+//    private func getResumeDataURL() -> URL {
+//        let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+//        let resumeDataFileName = url.lastPathComponent + ".resumeData"
+//        return cacheDirectory.appendingPathComponent(resumeDataFileName)
+//    }
+//}
+//
+//// MARK: - URLSessionDownloadDelegate
+//extension DownloadOperation: URLSessionDownloadDelegate {
+//    /// 下载进度更新
+//    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+//        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
+//        progressHandler?(progress)
+//    }
+//    
+//    /// 下载完成
+//    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+//        if !isPaused {
+//            // 将文件移动到缓存目录
+//            let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+//            let fileName = url.lastPathComponent
+//            let destinationURL = cacheDirectory.appendingPathComponent(fileName)
+//            try? FileManager.default.moveItem(at: location, to: destinationURL)
+//            completionHandler?(destinationURL, nil)
+//        }
+//    }
+//    
+//    /// 任务完成(包括成功或失败)
+//    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+//        if let error = error {
+//            completionHandler?(nil, error)
+//        }
+//    }
+//}

+ 3 - 3
AIRingtone/Common/Tool/OperationQueue/TSGenerateBaseOperation/TSGeneratePhotoOperation.swift

@@ -44,9 +44,9 @@ class TSGeneratePhotoOperation: TSGenerateBaseOperation , @unchecked Sendable{
 
     override func replaceSaveInfoModel(model:TSActionInfoModel){
         model.uuid = uuid
-        TSPhotoHistory.replaceModel(oldID: currentActionInfoModel.id, newModel: model)
+        TSPhotoHistory.shared.replaceModel(oldID: currentActionInfoModel.id, newModel: model)
         currentActionInfoModel = model
-        dePrint("TSPhotoHistory.listModelArray.count=\(TSPhotoHistory.listModelArray.count)")
+        dePrint("TSPhotoHistory.shared.listModels.count=\(TSPhotoHistory.shared.listModels.count)")
         dePrint("model actionStatus 发出=\(model.actionStatus)")
         currentActionInfoModelChanged?(currentActionInfoModel)
     }
@@ -93,7 +93,7 @@ class TSGeneratePhotoOperation: TSGenerateBaseOperation , @unchecked Sendable{
 //                self.replaceSaveInfoModel(model: self.currentActionInfoModel)
 //                self.stateDatauPblished = (.failed("error?.localizedDescription"),nil)
 //            }
-////            TSPhotoHistory.dePrintAllModel()
+////            TSPhotoHistory.shared.dePrintAllModel()
 //        }
 //    }
     

+ 3 - 3
AIRingtone/Common/Tool/OperationQueue/TSGenerateBaseOperation/TSGeneratePosterOperation.swift

@@ -45,9 +45,9 @@ class TSGeneratePosterOperation: TSGenerateBaseOperation , @unchecked Sendable{
 
     override func replaceSaveInfoModel(model:TSActionInfoModel){
         model.uuid = uuid
-        TSPosterHistory.replaceModel(oldID: currentActionInfoModel.id, newModel: model)
+        TSPosterHistory.shared.replaceModel(oldID: currentActionInfoModel.id, newModel: model)
         currentActionInfoModel = model
-        dePrint("TSPosterHistory.listModelArray.count=\(TSPosterHistory.listModelArray.count)")
+        dePrint("TSPosterHistory.shared.listModels.count=\(TSPosterHistory.shared.listModels.count)")
         dePrint("model actionStatus 发出=\(model.actionStatus)")
         currentActionInfoModelChanged?(currentActionInfoModel)
     }
@@ -94,7 +94,7 @@ class TSGeneratePosterOperation: TSGenerateBaseOperation , @unchecked Sendable{
 //                self.replaceSaveInfoModel(model: self.currentActionInfoModel)
 //                self.stateDatauPblished = (.failed("error?.localizedDescription"),nil)
 //            }
-////            TSPosterHistory.dePrintAllModel()
+////            TSPosterHistory.shared.dePrintAllModel()
 //        }
 //    }
     

+ 3 - 3
AIRingtone/Common/Tool/OperationQueue/TSGenerateBaseOperation/TSGenerateRintoneOperation.swift

@@ -46,9 +46,9 @@ class TSGenerateRintoneOperation: TSGenerateBaseOperation , @unchecked Sendable{
     
     override func replaceSaveInfoModel(model:TSActionInfoModel){
         model.uuid = uuid
-        TSAIRintoneHistory.replaceModel(oldID: currentActionInfoModel.id, newModel: model)
+        TSAIRintoneHistory.shared.replaceModel(oldID: currentActionInfoModel.id, newModel: model)
         currentActionInfoModel = model
-        dePrint("TSAIRintoneHistory.listModelArray.count=\(TSAIRintoneHistory.listModelArray.count)")
+        dePrint("TSAIRintoneHistory.shared.listModels.count=\(TSAIRintoneHistory.shared.listModels.count)")
         dePrint("model actionStatus 发出=\(model.actionStatus)")
         currentActionInfoModelChanged?(currentActionInfoModel)
     }
@@ -92,7 +92,7 @@ class TSGenerateRintoneOperation: TSGenerateBaseOperation , @unchecked Sendable{
 //                self.replaceSaveInfoModel(model: self.currentActionInfoModel)
 //                self.stateDatauPblished = (.failed("error?.localizedDescription"),nil)
 //            }
-//            TSAIRintoneHistory.dePrintAllModel()
+//            TSAIRintoneHistory.shared.dePrintAllModel()
 //        }
 //    }
     

+ 2 - 2
AIRingtone/Common/Tool/TSAudioPlayer/TSAudioPlayer.swift

@@ -56,7 +56,7 @@ class TSAudioPlayer: NSObject {
         return player != nil
     }
     
-    var currentTimeChanged: ((Double) -> Void)?
+    var currentTimeChanged: ((Double,Double) -> Void)?
     var audioPlayerDidFinishHandle: ((Bool) -> Void)?
     
     /// 初始化播放器
@@ -150,7 +150,7 @@ class TSAudioPlayer: NSObject {
         timeObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
             guard let self = self else { return }
             let currentTime = CMTimeGetSeconds(time)
-            self.currentTimeChanged?(currentTime)
+            self.currentTimeChanged?(currentTime,duration)
         }
     }
     

+ 106 - 19
AIRingtone/Common/Tool/TSAudioPlayer/TSBusinessAudioPlayer.swift

@@ -5,7 +5,7 @@
 //  Created by 100Years on 2025/3/7.
 //
 
-
+import AVFoundation
 class TSBusinessAudioPlayer {
     
     static let shared = TSBusinessAudioPlayer()
@@ -22,6 +22,8 @@ class TSBusinessAudioPlayer {
     private var audioPlayer: TSAudioPlayer?
     
     var stateChangedHandle:((PlayerState) -> Void)?
+    var currentTimeChangedHandle:((Double,Double) -> Void)?
+    
     var currentPlayerState:PlayerState = .stop
     var duration:Double{
         if let audioPlayer = audioPlayer {
@@ -53,6 +55,11 @@ class TSBusinessAudioPlayer {
         return 0.0
     }
     
+    /// 跳转到指定时间
+    /// - Parameter time: 目标时间(秒)
+    func seek(to time: Double) {
+        audioPlayer?.seek(to: time)
+    }
     
     var playProgress:Double{
         let playProgress = currentTime / duration
@@ -68,36 +75,43 @@ class TSBusinessAudioPlayer {
         return false
     }
     var currentURLString:String = ""
+    var currentLocalURL:URL? = nil
     var currentIndexPath:IndexPath? = nil
     
     //加载音乐可能 2-3 秒有结果,停止加载后播放.
     private var isStopPlayingAfterLoading:Bool = false
 
-    func isPlayURLString(string:String,indexPath:IndexPath? = nil) -> Bool {
+    func isPlayURLString(string:String,localURL:URL? = nil,indexPath:IndexPath? = nil) -> Bool {
 
         if currentURLString == string {
+            
             if let currentIndexPath = currentIndexPath,
                let indexPath = indexPath,
                indexPath != currentIndexPath
             {
                 return false
-            }else{
+            }else if let currentLocalURL = currentLocalURL,
+                let localURL = localURL,
+                currentLocalURL != localURL
+             {
+                 return false
+             }else{
                 return true
             }
         }
         return false
     }
     
+    func loadLoactionURL(url:URL){
+        self.audioPlayer = TSAudioPlayer(url: url)
+    }
 
-    func playUrlString(_ urlString:String?,loop:Bool = false,indexPath:IndexPath? = nil) {
+    func playUrlString(_ urlString:String?,localURL:URL? = nil,loop:Bool = false,indexPath:IndexPath? = nil) {
         self.stop()
         if let urlString = urlString {
             
-//            if self.currentURLString == urlStrin {
-//                self.play()
-//                return
-//            }
             self.currentURLString = urlString
+            self.currentLocalURL = localURL
             self.currentIndexPath = indexPath
 
             let palyFile:(URL)->Void = { [weak self] url in
@@ -111,11 +125,9 @@ class TSBusinessAudioPlayer {
                     setVolume(volume: 1.0)
                 }
                 
-                self.audioPlayer?.currentTimeChanged = { [weak self] currentTime in
+                self.audioPlayer?.currentTimeChanged = { [weak self] currentTime,duration in
                     guard let self = self else { return }
-                    
-                    
-                    
+                    currentTimeChangedHandle?(currentTime,duration)
                     changePlayerState(.currentTime(currentTime))
                 }
                 
@@ -131,12 +143,18 @@ class TSBusinessAudioPlayer {
             }
             
             isStopPlayingAfterLoading = false
-            if let path = TSCommonTool.getCachedURLString(from: urlString,missingEx: "mp3") {
+            
+            if let path = self.currentLocalURL,TSFileManagerTool.fileExists(at: path){
+                palyFile(path) //播放
+                
+            }else if let path = TSDownloadManager.getRingLocalURL(urlString: urlString) {
                 palyFile(path) //播放
             }else{
                 self.changePlayerState(.loading(0.0))
-                TSCommonTool.downloadAndCacheFile(from: urlString,missingEx: "mp3") { [weak self] path, error in
+                
+                _ = TSDownloadManager.downloadFile(urlString: urlString,missingEx: "mp3") {[weak self] url, error in
                     guard let self = self else { return }
+                    
                     self.changePlayerState(.loading(1.0))
                 
                     if isStopPlayingAfterLoading == true || currentURLString != urlString{
@@ -144,8 +162,8 @@ class TSBusinessAudioPlayer {
                         return
                     }
                     
-                    if let path = path {
-                        palyFile(URL(fileURLWithPath: path)) //播放
+                    if let url = url {
+                        palyFile(url) //播放
                     }else{
                         //暂停
                         self.stop()
@@ -176,8 +194,7 @@ class TSBusinessAudioPlayer {
     }
     
     func setVolume(volume:Float){
-        self.audioPlayer?.volume = volume 
-//        self.audioPlayer?.setVolume(volume)
+        self.audioPlayer?.volume = volume
         changePlayerState(.volume(volume))
     }
     
@@ -196,7 +213,7 @@ class TSBusinessAudioPlayer {
         currentPlayerState = state
         kExecuteOnMainThread{
             self.stateChangedHandle?(state)
-            NotificationCenter.default.post(name: .kBusinessAudioStateChange, object: nil, userInfo: ["PlayerState": state])
+//            NotificationCenter.default.post(name: .kBusinessAudioStateChange, object: nil, userInfo: ["PlayerState": state])
         }
     }
      
@@ -204,3 +221,73 @@ class TSBusinessAudioPlayer {
         dePrint("TSAudioPlayer TSBusinessAudioPlayer deinit")
     }
 }
+
+
+extension TSBusinessAudioPlayer{
+    struct AudioFileInfo {
+        let sizeInBytes: UInt64?    // 文件大小(字节)
+        let durationInSeconds: Double?  // 音频时长(秒)
+    }
+
+    static func getAudioFileInfo(path: String) -> AudioFileInfo? {
+        // 1. 检查URL有效性
+        guard let url = URL(string: path) else {
+            print("getAudioFileInfo 无效的URL字符串")
+            return nil
+        }
+        
+        // 2. 检查文件是否存在(仅限本地文件)
+        guard FileManager.default.fileExists(atPath: url.path) else {
+            print("getAudioFileInfo 文件不存在或不是本地路径")
+            return nil
+        }
+        
+        // 3. 获取文件大小
+        let fileSize: UInt64? = {
+            do {
+                let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
+                return attributes[.size] as? UInt64
+            } catch {
+                print("获取文件大小失败: \(error.localizedDescription)")
+                return nil
+            }
+        }()
+        
+        // 4. 获取音频时长
+        let duration: Double? = {
+            
+            return getAudioDurationWithAudioFile(url: url)
+//            let asset = AVURLAsset(url: url)
+//            let seconds = Double(CMTimeGetSeconds(asset.duration))
+//            return seconds.isNaN ? nil : seconds
+        }()
+        
+        return AudioFileInfo(sizeInBytes: fileSize, durationInSeconds: duration)
+    }
+    
+    /// 同步获取音频时长(可能阻塞线程!)
+    /// 使用 AudioFile 同步获取音频时长
+    static func getAudioDurationWithAudioFile(url: URL) -> TimeInterval? {
+        var audioFile: AudioFileID?
+        let status = AudioFileOpenURL(url as CFURL, .readPermission, 0, &audioFile)
+        
+        guard status == noErr, let file = audioFile else {
+            print("⚠️ 打开音频文件失败: \(status)")
+            return nil
+        }
+        
+        // 获取音频时长(单位:秒)
+        var duration: Float64 = 0
+        var propertySize = UInt32(MemoryLayout.size(ofValue: duration))
+        let durationStatus = AudioFileGetProperty(
+            file,
+            kAudioFilePropertyEstimatedDuration,
+            &propertySize,
+            &duration
+        )
+        
+        AudioFileClose(file)
+        
+        return durationStatus == noErr ? duration : nil
+    }
+}

+ 78 - 104
AIRingtone/Common/Tool/TSBandRingTool/AudioTool.swift

@@ -6,7 +6,7 @@
 //
 
 import AVFoundation
-
+import ffmpegkit
 typealias AudioCompletionHandler = (URL?, String?) -> Void
 
 class AudioTool {
@@ -14,132 +14,102 @@ class AudioTool {
     lazy var queue = DispatchQueue(label: "queue_audio_convert")
     
     /*
-     剪切开始工作
-     大概流程
-    1、获得视频总时长,处理时间,数组格式返回音频数据
-    2、创建导出会话
-    3、设计导出时间范围,淡出时间范围
-    4、设计新音频配置数据,文件路径,类型等
-    5、开始剪切
-     */
-    
+      剪切开始工作
+      大概流程
+     1、获得视频总时长,处理时间,数组格式返回音频数据
+     2、创建导出会话
+     3、设计导出时间范围,淡出时间范围
+     4、设计新音频配置数据,文件路径,类型等
+     5、开始剪切
+      */
+
     /// 裁剪音频
     /// - Parameters:
     ///   - asset: 音频资源
     ///   - startDuration: 开始时间
     ///   - endDuration: 结束时间
-    ///   - fadeIn: 淡入时长
-    ///   - fadeOut: 淡出时长
+    ///   - fadeIn: 淡入时长(暂时失效)
+    ///   - fadeOut: 淡出时长(暂时失效)
+    ///   - volume: volume
     ///   - filePath: 导出保存路径
     ///   - completionHandler: String: 导出文件存储路径,String: 报错信息
-    func cutAudio(asset: AVAsset,
-                  from startDuration: Double, to endDuration: Double,
-                  fadeIn: Double, fadeOut: Double,
-                  completionHandler: ((String?, String?) -> Void)?) {
+    func cutAudio(inputFilePath: String,
+                  startDuration: Double,
+                  endDuration: Double,
+                  fadeIn: Double,
+                  fadeOut: Double,
+                  addVolume: Double,
+                  outputFilePath: String,
+                  completionHandler: @escaping (String?, String?) -> Void)
+    {
+        let inPath = escapeFilePath(inputFilePath)
+        let outPath = escapeFilePath(outputFilePath)
         
-        let tracks = asset.tracks(withMediaType: .audio)
-        guard let track = tracks.first else {
-            // 如果没有数据,跳出
-            completionHandler?(nil, "Sorry, Unknow Error, Please try another")
-            return
-        }
-        // 2. 创建导出会话
-        guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A) else {
-            // 创建失败,则跳出
-            completionHandler?(nil, "Sorry, Unknow Error, Please try another")
-            return
-        }
-        
-        // 导出时间范围
-        // create trim time range // CMTimeMake(第几帧, 帧率)
-        let startTime = CMTime(value: CMTimeValue(startDuration), timescale: 1)
-        let endTime = CMTime(value: CMTimeValue(endDuration), timescale: 1)
-        let exportTimeRange = CMTimeRange(start: startTime, end: endTime)
-        
-        // 创建混音
-        let exportMix = AVMutableAudioMix()
-        let inputParameters = AVMutableAudioMixInputParameters(track: track)
-        
-        // 淡入时间
-        if fadeIn > 0, fadeIn < (endDuration - startDuration) {
-            let startFadeInTime = startTime
-            let endFadeInTime = CMTime(value: CMTimeValue(startDuration+fadeIn), timescale: 1)
-            let fadeInTimeRange = CMTimeRange(start: startFadeInTime, end: endFadeInTime)
-            inputParameters.setVolumeRamp(fromStartVolume: 0.0, toEndVolume: 1.0, timeRange: fadeInTimeRange)
-        }
-        // 淡出时间
-        if fadeOut > 0, fadeOut < (endDuration - startDuration) {
-            let startFadeOutTime = CMTime(value: CMTimeValue(endDuration-fadeOut), timescale: 1)
-            let endFadeOutTime = endTime
-            let fadeOutTimeRange = CMTimeRange(start: startFadeOutTime, end: endFadeOutTime)
-            inputParameters.setVolumeRamp(fromStartVolume: 1.0, toEndVolume: 0.0, timeRange: fadeOutTimeRange)
-        }
+//        let command = "-i \(inPath) -vn -ss \(startDuration) -to \(endDuration) -c:a libmp3lame \(outPath)"
+//        let command1 = "-i \(inPath) -vn -ss \(startDuration) -to \(endDuration) -filter:a \"volume=2.0\" -c:a libmp3lame \(outPath)"
+
+        // 1. 计算裁剪后的音频时长(秒)
+        let startSec = startDuration
+        let endSec = endDuration
+        let segmentDuration = endSec - startSec  // 例如:30-10=20秒
         
-        exportMix.inputParameters = [inputParameters]
+        // 2. 计算淡出开始时间(裁剪后的最后 fadeOutDuration 秒)
+        let fadeOutStartTime = segmentDuration - fadeOut // 20-5=15秒
         
-        guard let documentURL = TSBandRingTool.ringDirectory else {
-            return
-        }
-        let outputFilePath = NSString(string: documentURL).appendingPathComponent("ringExportM4a.m4a")
+        // 3. 构建正确的 FFmpeg 命令(关键:-ss 和 -to 必须在 -i 之前!)
+        let command = """
+        -y -ss \(startDuration) -to \(endDuration) -i \(inPath) -vn \
+        -af "afade=in:st=0:d=\(fadeIn), afade=out:st=\(fadeOutStartTime):d=\(fadeOut), volume=\(addVolume)" \
+        -c:a libmp3lame \(outPath)
+        """
         
-        if FileManager.default.fileExists(atPath: outputFilePath) {
-            try? FileManager.default.removeItem(atPath: outputFilePath)
-        }
+        dePrint("执行命令:\n\(command)")
         
-        // output path 新文件路径
-        exportSession.outputURL = URL(fileURLWithPath: outputFilePath)
-        exportSession.outputFileType = .m4a
-        exportSession.timeRange = exportTimeRange
-        // 新的混音音频
-        exportSession.audioMix = exportMix
-        
-        // 开始导出
-        exportSession.exportAsynchronously {
-            if let error = exportSession.error {
-                debugPrint(error)
+        FFmpegKit.executeAsync(command) { session in
+            guard let session = session else { return }
+
+            if ReturnCode.isSuccess(session.getReturnCode()) {
+                dePrint("FFmpegKit ✅ 处理成功")
+            } else {
+                dePrint("FFmpegKit ❌ 处理失败。错误码:\(session.getReturnCode())")
             }
-            switch exportSession.status {
+            
+            let state = session.getState()
+            switch state {
             case .completed:
-                completionHandler?(outputFilePath, nil)
+                completionHandler(outputFilePath, nil)
             case .failed:
-                completionHandler?(nil, "Sorry, Export Session Failure")
+                if let error = session.getFailStackTrace() {
+                    completionHandler(nil, "Sorry, Extract Audio Failure".localized)
+                } else {
+                    completionHandler(nil, "Sorry, Extract Audio Failure".localized)
+                }
             default:
-                completionHandler?(nil, "Sorry, Export Session Failure")
+                completionHandler(nil, "Sorry, Extract Audio Failure".localized)
             }
         }
     }
     
+    func escapeFilePath(_ path: String) -> String {
+        return "\"\(path)\""
+    }
+    
     /*
      音频格式转换
      1. 裁剪后获得m4a
      1. m4a 转 wav
      2. wav 转 mp3
      */
-    func startTansformAudio(asset: AVAsset,
-                  from startDuration: Double, to endDuration: Double,
-                  fadeIn: Double, fadeOut: Double,
-                  savePath filePath: String,
-                  completionHandler: ((String?, String?) -> Void)?) {
-        
-        cutAudio(asset: asset, from: startDuration, to: endDuration, fadeIn: fadeIn, fadeOut: fadeOut) { outputFilePath, errMsg in
-            
-            guard let outputFilePath = outputFilePath,
-                  let documentURL = TSBandRingTool.ringDirectory else {
-                completionHandler?(nil, errMsg)
-                return
-            }
-            let wavFilePath = NSString(string: documentURL).appendingPathComponent("ringExportWav.wav")
-                
-            AudioConverter.convertM4a(toWav: outputFilePath, outPath: wavFilePath) { path, errMsg in
-                
-                guard let path = path else {
-                    completionHandler?(nil, errMsg)
-                    return
-                }
-                AudioConverter.convenrtToMp3(withResult: path, outPath: filePath) { mp3Path, errMsg in
-                    completionHandler?(mp3Path, errMsg)
-                }
-            }
+    func startTansformAudio(url: String,
+                            from startDuration: Double,
+                            to endDuration: Double,
+                            fadeIn: Double,
+                            fadeOut: Double,
+                            addVolume: Double,
+                            savePath filePath: String,
+                            completionHandler: ((String?, String?) -> Void)?) {
+        cutAudio(inputFilePath: url, startDuration: startDuration, endDuration: endDuration, fadeIn: fadeIn, fadeOut: fadeOut,addVolume:addVolume, outputFilePath: filePath) { path, error in
+            completionHandler?(path, error)
         }
     }
     
@@ -171,9 +141,13 @@ class AudioTool {
             completion?(nil)
             return
         }
-        let url = URL(fileURLWithPath: copyBandURL.path)
-        debugPrint("===bandPath: \(url.absoluteString)")
-        completion?(url)
+        
+        completion?(copyBandURL)
+        debugPrint("===bandPath: \(copyBandURL)")
+        
+//        let url = URL(fileURLWithPath: copyBandURL.path)
+//        debugPrint("===bandPath: \(url)")
+//        completion?(url)
     }
     
     /*

+ 44 - 25
AIRingtone/Common/Tool/TSBandRingTool/TSBandRingTool.swift

@@ -56,29 +56,32 @@ class TSBandRingTool:NSObject {
     }
     
     func shareBandVC(vc:UIViewController,
-        fileURLString: String,
+                     fileURL: URL,
                    fileName:String?,
                    completion: ((Bool) -> Void)? = nil) {
         self.targetVC = vc
+
         if checkGarageBandInstallation() == false {
             completion?(false)
             return
         }
-
-        if fileURLString.contains("http") {
+        if TSFileManagerTool.fileExists(at: fileURL) == false{
+            completion?(false)
+            return
+        }
+        
+//        if fileURLString.contains("http") {
             
-//            TSLoadingAnimation.showLoading(in: self.targetVC?.view)
-            if let window = WindowHelper.getKeyWindow() {
-                window.addSubview(ringLoadingView)
-                ringLoadingView.isRotating = true
-            }
-
-            TSCommonTool.downloadAndCacheFile(from: fileURLString,missingEx: "mp3") { [weak self] path, error in
-                guard let self = self else { return }
-//                TSLoadingAnimation.hideLoading()
-                ringLoadingView.removeFromSuperview()
-                if let path = path,let url = URL(string: path) {
-                    self.createBand(with: url, fileName: fileName) { [weak self]  bandURL in
+//            if let window = WindowHelper.getKeyWindow() {
+//                window.addSubview(ringLoadingView)
+//                ringLoadingView.isRotating = true
+//            }
+            
+//            _ = TSDownloadManager.downloadFile(urlString: fileURLString,missingEx: "mp3") {[weak self] url, error in
+//                guard let self = self else { return }
+//                ringLoadingView.removeFromSuperview()
+        
+                    self.createBand(with: fileURL, fileName: fileName) { [weak self]  bandURL in
                         guard let self = self else { return }
                         if let url = bandURL {
                             completion?(true)
@@ -88,15 +91,29 @@ class TSBandRingTool:NSObject {
                             dePrint("Failed to set, please try another")
                         }
                     }
-                }else{
-                    dePrint("downloadAndCacheFile = \(error?.localizedDescription)")
-                    completion?(false)
-                }
-            }
-        }else{
-            dePrint("ringtone no http")
-            completion?(false)
-        }
+//            TSCommonTool.downloadAndCacheFile(from: fileURLString) { [weak self] path, error in
+//                guard let self = self else { return }
+////                TSLoadingAnimation.hideLoading()
+//                ringLoadingView.removeFromSuperview()
+//                if let path = path,let url = URL(string: path) {
+//                    self.createBand(with: url, fileName: fileName) { bandURL in
+//                        if let url = bandURL {
+//                            completion?(true)
+//                            self.shareRing(fileUrl: url)
+//                        }else{
+//                            completion?(false)
+//                            dePrint("Failed to set, please try another")
+//                        }
+//                    }
+//                }else{
+//                    dePrint("downloadAndCacheFile = \(error?.localizedDescription)")
+//                    completion?(false)
+//                }
+//            }
+//        }else{
+//            dePrint("ringtone no http")
+//            completion?(false)
+//        }
                                    
        
     }
@@ -122,6 +139,8 @@ class TSBandRingTool:NSObject {
     
     func shareRing(fileUrl: URL) {
         DispatchQueue.main.async {
+            guard let targetVC = self.targetVC else { return }
+            
             self.showTutorialVideo()
             
             let vc = UIActivityViewController(activityItems: [fileUrl], applicationActivities: nil)
@@ -143,7 +162,7 @@ class TSBandRingTool:NSObject {
 //                .airDrop,
 //                .openInIBooks
 //            ]
-            self.targetVC?.present(vc, animated: true, completion: {
+            targetVC.present(vc, animated: true, completion: {
                 self.tryStartPictureInPicture()
             })
             

+ 55 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHAudioProcessing.swift

@@ -0,0 +1,55 @@
+//
+//  ZHAudioProcessing.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+
+struct ZHAudioProcessing {
+    
+    typealias BufferRefSuccessHandler = (NSMutableData) -> Swift.Void
+    typealias BufferRefFailureHandler = (Error?) -> Swift.Void
+    
+    public static func bufferRef(
+        asset: AVAsset,
+        track: AVAssetTrack,
+        success: BufferRefSuccessHandler?,
+        failure: BufferRefFailureHandler?
+        ) {
+        let data = NSMutableData()
+        let dict: [String: Any] = [AVFormatIDKey: kAudioFormatLinearPCM,
+                                   AVLinearPCMIsBigEndianKey: false,
+                                   AVLinearPCMIsFloatKey: false,
+                                   AVLinearPCMBitDepthKey: 16]
+        do {
+            let reader: AVAssetReader = try AVAssetReader(asset: asset)
+            let output: AVAssetReaderTrackOutput = AVAssetReaderTrackOutput(track: track, outputSettings: dict)
+            reader.add(output)
+            reader.startReading()
+            while reader.status == .reading {
+                if let sampleBuffer: CMSampleBuffer = output.copyNextSampleBuffer() {
+                    if let blockBuffer: CMBlockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) {
+                        let length: Int = CMBlockBufferGetDataLength(blockBuffer)
+                        var sampleBytes = [Int16](repeating: Int16(), count: length)
+                        CMBlockBufferCopyDataBytes(blockBuffer, atOffset: 0, dataLength: length, destination: &sampleBytes)
+//                        CMBlockBufferCopyDataBytes(blockBuffer, 0, length, &sampleBytes)
+                        data.append(&sampleBytes, length: length)
+                        CMSampleBufferInvalidate(sampleBuffer)
+                    }
+                }
+            }
+            if reader.status == .completed {
+                print("读取结束")
+                success?(data)
+            } else {
+                failure?(nil)
+            }
+        } catch let err {
+            failure?(err)
+        }
+    }
+}

+ 37 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHCroppedDelegate.swift

@@ -0,0 +1,37 @@
+//
+//  ZHCroppedDelegate.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol ZHCroppedDelegate {
+    /**
+     start cropped
+     */
+    @objc optional func waveformView(startCropped: UIView, progress rate: CGFloat)
+    
+    /**
+     end cropped
+     */
+    @objc optional func waveformView(endCropped: UIView, progress rate: CGFloat)
+    
+    /**
+     will
+     */
+    @objc optional func waveformView(croppedStartDragging cropped: UIView)
+    
+    /**
+     ing
+     */
+    @objc optional func waveformView(croppedDragIn cropped: UIView)
+    
+    /**
+     ed
+     */
+    @objc optional func waveformView(croppedDragFinish cropped: UIView)
+    
+}

+ 36 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHTrackProcessing.swift

@@ -0,0 +1,36 @@
+//
+//  ZHTrackProcessing.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+
+struct ZHTrackProcessing {
+    public static func cutAudioData(size: CGSize, recorder data: NSMutableData, scale: CGFloat) -> [CGFloat] {
+        var filteredSamplesMA: [CGFloat] = []
+        let sampleCount = data.length / MemoryLayout<Int>.size
+        let binSize = CGFloat(sampleCount) / (size.width * scale)
+        var i = 0
+        while i < sampleCount {
+            let rangeData = data.subdata(with: NSRange(location: i, length: 1))
+            let item = rangeData.withUnsafeBytes({ (ptr: UnsafePointer<Int>) -> Int in
+                return ptr.pointee
+            })
+            filteredSamplesMA.append(CGFloat(item))
+            i += Int(binSize)
+        }
+        return trackScale(size: size, source: filteredSamplesMA)
+    }
+    
+    private static func trackScale(size: CGSize, source: [CGFloat]) -> [CGFloat] {
+        if let max = source.max() {
+            let k = size.height / max
+            return source.map{ $0 * k }
+        }
+        return source
+    }
+}

+ 311 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHWaveformView.swift

@@ -0,0 +1,311 @@
+//
+//  ZHWaveformView.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+
+public class ZHWaveformView: UIView {
+    
+    /** waves color */
+    public var wavesColor: UIColor = .red {
+        didSet {
+            DispatchQueue.main.async {
+                _ = self.trackLayer.map({ [unowned self] in
+                    $0.strokeColor = self.wavesColor.cgColor
+                })
+            }
+        }
+    }
+    
+    /** Cut off the beginning part color */
+    public var beginningPartColor: UIColor = .gray
+    
+    /** Cut out the end part color */
+    public var endPartColor: UIColor = .gray
+    
+    /** Track Scale normal 0.5, max 1*/
+    public var trackScale: CGFloat = 0.5 {
+        didSet {
+            if let `assetMutableData` = assetMutableData {
+                croppedViewZero()
+                trackProcessingCut = ZHTrackProcessing.cutAudioData(size: self.frame.size, recorder: assetMutableData, scale: trackScale)
+                drawTrack(
+                    with: CGRect(x: (startCroppedView?.bounds.width ?? 0),
+                                 y: 0,
+                                 width: self.frame.width - (startCroppedView?.bounds.width ?? 0) - (endCroppedView?.bounds.width ?? 0),
+                                 height: self.frame.height),
+                    filerSamples: trackProcessingCut ?? []
+                )
+            }
+        }
+    }
+    
+    public weak var croppedDelegate: ZHCroppedDelegate? {
+        didSet { layoutIfNeeded() }
+    }
+    
+    public weak var waveformDelegate: ZHWaveformViewDelegate?
+    
+    private var fileURL: URL
+    
+    private var asset: AVAsset?
+    
+    private var track: AVAssetTrack?
+    
+    private var trackLayer: [CAShapeLayer] = []
+    
+    private var startCroppedView: UIView?
+    
+    private var endCroppedView: UIView?
+    
+    private var leftCorppedCurrentX: CGFloat = 0
+    
+    private var rightCorppedCurrentX: CGFloat = 0
+    
+    private var trackWidth: CGFloat = 0
+    
+    private var startCorppedIndex: Int = 0
+    
+    private var endCorppedIndex: Int = 0
+    
+    private var trackProcessingCut: [CGFloat]?
+    
+    private var assetMutableData: NSMutableData?
+    
+    private(set) var startCroppedRate: CGFloat = 0
+    private(set) var endCroppedRate: CGFloat = 0
+    
+    public init(frame: CGRect, fileURL: URL) {
+        self.fileURL = fileURL
+        super.init(frame: frame)
+        waveformDelegate?.waveformViewStartDrawing?(waveformView: self)
+        backgroundColor = .white
+        
+        let asset = AVAsset(url: fileURL)
+        guard let track = asset.tracks(withMediaType: .audio).first else {
+            return
+        }
+        self.asset = asset
+        self.track = track
+        ZHAudioProcessing.bufferRef(asset: asset, track: track, success: { [unowned self] (data) in
+            self.assetMutableData = data
+            self.trackProcessingCut = ZHTrackProcessing.cutAudioData(size: frame.size, recorder: data, scale: self.trackScale)
+            self.drawTrack(with: CGRect(origin: .zero, size: frame.size), filerSamples: self.trackProcessingCut ?? [])
+            self.waveformDelegate?.waveformViewDrawComplete?(waveformView: self)
+        }) { (error) in
+            assert(true, error?.localizedDescription ?? "Error, AudioProcessing.bufferRef")
+        }
+    }
+    
+    override public func layoutIfNeeded() {
+        super.layoutIfNeeded()
+        if let samples = trackProcessingCut {
+            creatCroppedView()
+            drawTrack(
+                with: CGRect(x: startCroppedView?.bounds.width ?? 0,
+                             y: 0,
+                             width: frame.width - (startCroppedView?.bounds.width ?? 0) - (endCroppedView?.bounds.width ?? 0),
+                             height: frame.height),
+                filerSamples: samples
+            )
+        }
+    }
+    
+    private func drawTrack(with rect: CGRect, filerSamples: [CGFloat]) {
+        _ = trackLayer.map{ $0.removeFromSuperlayer() }
+        trackLayer.removeAll()
+        startCroppedView?.removeFromSuperview()
+        endCroppedView?.removeFromSuperview()
+        // bezier width
+        trackWidth = rect.width / (CGFloat(filerSamples.count - 1) + CGFloat(filerSamples.count))
+        endCorppedIndex = filerSamples.count
+        for t in 0..<filerSamples.count {
+            let layer = CAShapeLayer()
+            layer.frame = CGRect(
+                x: CGFloat(t) * trackWidth * 2 + (startCroppedView?.bounds.width ?? 0),
+                y: 0,
+                width: trackWidth,
+                height: rect.height
+            )
+            layer.lineCap = CAShapeLayerLineCap.butt
+            layer.lineJoin = CAShapeLayerLineJoin.round
+            layer.lineWidth = trackWidth
+            layer.strokeColor = wavesColor.cgColor
+            self.layer.addSublayer(layer)
+            self.trackLayer.append(layer)
+        }
+        
+        for i in 0..<filerSamples.count {
+            let itemLinePath = UIBezierPath()
+            let y: CGFloat = (rect.height - filerSamples[i]) / 2
+            let height: CGFloat = filerSamples[i] + y
+            itemLinePath.move(to: CGPoint(x: 0, y: y))
+            itemLinePath.addLine(to: CGPoint(x: 0, y: height))
+            itemLinePath.close()
+            itemLinePath.lineWidth = trackWidth
+            let itemLayer = trackLayer[i]
+            itemLayer.path = itemLinePath.cgPath
+        }
+        if let l = startCroppedView {
+            addSubview(l)
+        }
+        if let r = endCroppedView {
+            addSubview(r)
+        }
+        
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+}
+
+extension ZHWaveformView {
+    
+    private func croppedViewZero() {
+        if let leftCropped = startCroppedView {
+            leftCropped.frame = CGRect(x: 0, y: leftCropped.frame.origin.y, width: leftCropped.bounds.width, height: leftCropped.bounds.height)
+        }
+        if let rightCropped = endCroppedView {
+            rightCropped.frame = CGRect(x: bounds.width - rightCropped.bounds.width, y: rightCropped.frame.origin.y, width: rightCropped.bounds.width, height: rightCropped.bounds.height)
+        }
+        
+    }
+    
+    private func creatCroppedView() {
+//        if let leftCropped = croppedDelegate?.waveformView(startCropped: self) {
+//            leftCropped.frame = CGRect(x: 0, y: leftCropped.frame.origin.y, width: leftCropped.bounds.width, height: leftCropped.bounds.height)
+//            leftCorppedCurrentX = 0
+//            let leftPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.leftCroppedPanRecognizer(sender:)))
+//            leftCropped.addGestureRecognizer(leftPanRecognizer)
+//            leftCropped.isUserInteractionEnabled = true
+//            startCroppedView = leftCropped
+//        }
+        
+//        if let rightCropped = croppedDelegate?.waveformView(endCropped: self) {
+//            rightCropped.frame = CGRect(x: bounds.width - rightCropped.bounds.width, y: rightCropped.frame.origin.y, width: rightCropped.bounds.width, height: rightCropped.bounds.height)
+//            rightCorppedCurrentX = bounds.width - rightCropped.bounds.width
+//            let rightPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.rightCroppedPanRecognizer(sender:)))
+//            rightCropped.addGestureRecognizer(rightPanRecognizer)
+//            rightCropped.isUserInteractionEnabled = true
+//            endCroppedView = rightCropped
+//        }
+    }
+    
+    @objc private func leftCroppedPanRecognizer(sender: UIPanGestureRecognizer) {
+        let limitMinX: CGFloat = frame.minX
+        let limitMaxX: CGFloat = endCroppedView?.frame.minX ?? bounds.width
+        if sender.state == .began {
+            croppedDelegate?.waveformView?(croppedDragIn: startCroppedView ?? UIView())
+        } else if sender.state == .changed {
+            croppedDelegate?.waveformView?(croppedDragIn: startCroppedView ?? UIView())
+            let newPoint = sender.translation(in: self)
+            var center = startCroppedView?.center
+            center?.x = leftCorppedCurrentX + newPoint.x
+            guard (center?.x ?? 0) > limitMinX && (center?.x ?? 0) < limitMaxX else { return }
+            startCroppedView?.center = center ?? .zero
+        } else if sender.state == .ended || sender.state == .failed {
+            croppedDelegate?.waveformView?(croppedDragFinish: startCroppedView ?? UIView())
+            leftCorppedCurrentX = startCroppedView?.center.x ?? 0
+        }
+        if (startCroppedView?.frame.minX ?? 0) < 0 {
+            var leftFrame = startCroppedView?.frame
+            leftFrame?.origin.x = 0
+            startCroppedView?.frame = leftFrame ?? .zero
+        }
+        
+        if (startCroppedView?.frame.maxX ?? 0) > limitMaxX {
+            var leftFrame = startCroppedView?.frame
+            leftFrame?.origin.x = limitMaxX - (startCroppedView?.bounds.width ?? 0)
+            startCroppedView?.frame = leftFrame ?? .zero
+        } // floorf ceilf
+        let lenght = ceilf(Float((((startCroppedView?.frame.maxX ?? 0) - (startCroppedView?.bounds.width ?? 0)) / trackWidth)))
+        let bzrLenght = ceilf(lenght/2)
+        startCorppedIndex = Int(bzrLenght) > trackLayer.count ? trackLayer.count : Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width - (startCroppedView?.frame.width ?? 0) - (endCroppedView?.frame.width ?? 0)
+        croppedDelegate?.waveformView?(startCropped: startCroppedView ?? UIView(), progress: ((startCroppedView?.frame.maxX ?? 0) - 20)/bezierWidth)
+    }
+    
+    func updateLeftCroppedPosition(_ position: CGFloat) {
+        let lenght = ceilf(Float((position / trackWidth)))
+        let bzrLenght = ceilf(lenght/2)
+        startCorppedIndex = Int(bzrLenght) > trackLayer.count ? trackLayer.count : Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width
+        self.startCroppedRate = position/bezierWidth
+    }
+    
+    func updateRightCroppedPosition(_ position: CGFloat) {
+        let lenght = ceilf(Float(position / trackWidth))
+        let bzrLenght = floorf(lenght/2) < 0 ? 0 : ceilf(lenght/2)
+        endCorppedIndex = Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width
+        self.endCroppedRate = position/bezierWidth
+    }
+    
+    @objc private func rightCroppedPanRecognizer(sender: UIPanGestureRecognizer) {
+        let limitMinX: CGFloat = startCroppedView?.frame.maxX ?? 0
+        let limitMaxX: CGFloat = frame.maxX
+        if sender.state == .began {
+            croppedDelegate?.waveformView?(croppedStartDragging: endCroppedView ?? UIView())
+        } else if sender.state == .changed {
+            croppedDelegate?.waveformView?(croppedDragIn: endCroppedView ?? UIView())
+            let newPoint = sender.translation(in: self)
+            var center = endCroppedView?.center
+            center?.x = rightCorppedCurrentX + newPoint.x
+            guard (center?.x ?? 0) > limitMinX && (center?.x ?? 0) < limitMaxX else { return }
+            endCroppedView?.center = center ?? .zero
+        } else if sender.state == .ended || sender.state == .failed {
+            croppedDelegate?.waveformView?(croppedDragFinish: endCroppedView ?? UIView())
+            rightCorppedCurrentX = endCroppedView?.center.x ?? (bounds.width - (endCroppedView?.bounds.width ?? 0))
+        }
+        if (endCroppedView?.frame.maxX ?? 0) > frame.maxX {
+            var rightFrame = endCroppedView?.frame
+            rightFrame?.origin.x = frame.maxX - (endCroppedView?.bounds.width ?? 0)
+            endCroppedView?.frame = rightFrame ?? .zero
+        }
+        if (endCroppedView?.frame.minX ?? 0) < limitMinX {
+            var rightFrame = endCroppedView?.frame
+            rightFrame?.origin.x = limitMinX
+            endCroppedView?.frame = rightFrame ?? .zero
+        }
+        let lenght = ceilf(Float(((endCroppedView?.frame.minX ?? 0) - (startCroppedView?.bounds.width ?? 0)) / trackWidth))
+        let bzrLenght = floorf(lenght/2) < 0 ? 0 : ceilf(lenght/2)
+        endCorppedIndex = Int(bzrLenght)
+        self.croppedWaveform(start: startCorppedIndex, end: endCorppedIndex)
+        let bezierWidth = self.frame.width - (startCroppedView?.frame.width ?? 0) - (endCroppedView?.frame.width ?? 0)
+        croppedDelegate?.waveformView?(endCropped: endCroppedView ?? UIView(), progress: ((endCroppedView?.frame.minX ?? 0)-20)/bezierWidth)
+    }
+    
+    typealias TrackIndex = Int
+    
+    func croppedWaveform(start: TrackIndex, end: TrackIndex) {
+        guard start > 0, start < trackLayer.count,
+              end > start, end <= trackLayer.count else {
+            return
+        }
+        let beginLayers = trackLayer[0..<start]
+        let wavesLayers = trackLayer[start..<end]
+        let endLayers = trackLayer[end..<trackLayer.count]
+        DispatchQueue.main.async {
+            _ = beginLayers.map({ [unowned self] in
+                $0.strokeColor = self.beginningPartColor.cgColor
+            })
+            _ = wavesLayers.map({ [unowned self] in
+                $0.strokeColor = self.wavesColor.cgColor
+            })
+            _ = endLayers.map({ [unowned self] in
+                $0.strokeColor = self.endPartColor.cgColor
+            })
+        }
+    }
+    
+}

+ 19 - 0
AIRingtone/Common/Tool/TSBandRingTool/ZHWaveform/ZHWaveformViewDelegate.swift

@@ -0,0 +1,19 @@
+//
+//  ZHWaveformViewDelegate.swift
+//  ZHWaveform_Example
+//
+//  Created by wow250250 on 2018/1/2.
+//  Copyright © 2018年 wow250250. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol ZHWaveformViewDelegate {
+    
+    // start
+    @objc optional func waveformViewStartDrawing(waveformView: ZHWaveformView)
+    
+    // complete
+    @objc optional func waveformViewDrawComplete(waveformView: ZHWaveformView)
+    
+}

+ 7 - 0
AIRingtone/Common/Tool/TSBusinessCommon.swift

@@ -0,0 +1,7 @@
+//
+//  TSBusinessFileManager.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/26.
+//
+

+ 151 - 0
AIRingtone/Common/Tool/TSBusinessFileManager.swift

@@ -0,0 +1,151 @@
+//
+//  TSBusinessFileManager.swift
+//  AIRingtone
+//
+//  Created by 100Years on 2025/3/27.
+//
+
+class TSBusinessFileManager {
+    
+    /// 获取 Video 下载后保存的的文件件路径
+    static var saveRingPathURL:URL = {
+        let saveRingPathURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("ring")
+        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 = saveRingPathURL.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
+    }
+    
+}

+ 112 - 0
AIRingtone/Common/Tool/TSDownloadManager.swift

@@ -0,0 +1,112 @@
+//
+//  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 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{
+            TSRingLoadingView.shared.showWindow()
+            _ = TSDownloadManager.downloadFile(urlString:urlString,missingEx: "mp3") { url, error in
+                TSRingLoadingView.shared.remove()
+                if let path = url {
+                    complete(path,true)
+                }else{
+                    complete(nil,true)
+                }
+            }
+        }
+    }
+    
+    static func getDownLoadRing(urlString:String,complete:@escaping (URL?,Bool)->Void){
+        if let path = TSDownloadManager.getRingLocalURL(urlString: urlString) {
+            complete(path,false)
+        }else{
+            TSRingLoadingView.shared.showWindow()
+            _ = TSDownloadManager.downloadFile(urlString:urlString,missingEx: "mp3") { url, error in
+                TSRingLoadingView.shared.remove()
+                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)
+    }
+    
+}

+ 0 - 157
AIRingtone/Common/Tool/TSFileManagerTool.swift

@@ -1,157 +0,0 @@
-//
-//  TSFileManagerTool.swift
-//  TSLiveWallpaper
-//
-//  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
-    }()
-    
-
-    /// 获取沙盒 Documents 目录路径
-    static var documentsDirectory: URL {
-        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
-    }
-
-    /// 获取沙盒 Cache 目录路径
-    static var cacheDirectory: URL {
-        return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-    }
-
-    /// 获取沙盒 Temporary 目录路径
-    static var temporaryDirectory: URL {
-        return FileManager.default.temporaryDirectory
-    }
-
-    static func copyFileWithOverwrite(from sourceURL: URL, to targetURL: URL) {
-        let fileManager = FileManager.default
-        do {
-            removeItem(from: targetURL)
-            checkFolderAndCreate(from: targetURL)
-            try fileManager.copyItem(at: sourceURL, to: targetURL)
-            debugPrint("文件复制成功!")
-        } catch {
-            debugPrint("文件复制失败: \(error.localizedDescription)")
-        }
-    }
-    
-    static func removeItem(from sourceURL: URL) {
-        let fileManager = FileManager.default
-        do {
-            // 如果目标路径存在同名文件,先删除旧文件
-            if fileManager.fileExists(atPath: sourceURL.path) {
-                try fileManager.removeItem(at: sourceURL)
-            }
-            debugPrint("文件删除成功!")
-        } catch {
-            debugPrint("文件删除失败: \(error.localizedDescription)")
-        }
-    }
-    
-    /// 移动文件的方法(自动创建目标文件夹)
-    /// - Parameters:
-    ///   - sourceURL: 文件的源 URL
-    ///   - destinationURL: 目标 URL
-    /// - Throws: 如果移动失败,会抛出错误
-    static func moveFile(from sourceURL: URL, to destinationURL: URL) {
-        let fileManager = FileManager.default
-        
-        // 检查源文件是否存在
-        guard fileManager.fileExists(atPath: sourceURL.path) else {
-            let error = NSError(domain: "FileMoveError", code: 404, userInfo: [NSLocalizedDescriptionKey: "源文件不存在"])
-            debugPrint(error)
-            return
-        }
-        
-        // 获取目标文件夹的路径
-        let destinationDirectory = destinationURL.deletingLastPathComponent()
-        do {
-            // 如果目标文件夹不存在,创建文件夹
-            if !fileManager.fileExists(atPath: destinationDirectory.path) {
-                try fileManager.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
-            }
-            
-            // 检查目标路径是否已经存在文件
-            if fileManager.fileExists(atPath: destinationURL.path) {
-                // 如果需要覆盖,可以选择先删除目标文件
-                try fileManager.removeItem(at: destinationURL)
-            }
-            
-            // 尝试移动文件
-            try fileManager.moveItem(at: sourceURL, to: destinationURL)
-        } catch {
-            debugPrint("尝试移动文件失败: \(error.localizedDescription)")
-        }
-    }
-    
-    static func getFileName(from url: URL, includeExtension: Bool = true) -> String {
-        if includeExtension {
-            return url.lastPathComponent
-        } else {
-            return url.deletingPathExtension().lastPathComponent
-        }
-    }
-    
-    static func checkFolderAndCreate(from destinationURL: URL){
-        let fileManager = FileManager.default
-        let destinationDirectory = destinationURL.deletingLastPathComponent()
-        // 如果目标文件夹不存在,创建文件夹
-        if !fileManager.fileExists(atPath: destinationDirectory.path) {
-            do {
-                try fileManager.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
-            } catch {
-                debugPrint("尝试创建文件夹失败: \(error.localizedDescription)")
-            }
-        }
-    }
-
-    // MARK: - 文件操作方法
-
-    /// 检查文件或文件夹是否存在
-    static func fileExists(at url: URL) -> Bool {
-        return FileManager.default.fileExists(atPath: url.path)
-    }
-
-    /// 创建文件夹
-    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? {
-        let array = url.path.components(separatedBy:"/Caches/")
-        let cashFilePath = array.last
-        return cashFilePath
-    }
-    
-}
-
-extension String {
-    var fillCachePath:String{
-        return TSFileManagerTool.cacheDirectory.appendingPathComponent(self).path
-    }
-    
-    var fillCacheURL:URL{
-        return TSFileManagerTool.cacheDirectory.appendingPathComponent(self)
-    }
-}

+ 0 - 6
AIRingtone/Common/Tool/TSPublicContent.swift

@@ -1,6 +0,0 @@
-//
-//  TSPublicContent.swift
-//  AIRingtone
-//
-//  Created by 100Years on 2025/3/20.
-//

+ 4 - 0
Podfile

@@ -21,6 +21,10 @@ target 'AIRingtone' do
   pod 'TSSmalCoacopods', :path => '../TSSmalCoacopods'
   #pod 'TSSmalCoacopods',:git=>"http://hubgit.cn/zhouzhenshuai/TSSmalCoacopods.git", :branch => 'main'
   pod 'BetterSegmentedControl', '~> 2.0'
+#  pod 'ffmpeg-kit-ios-full', '~> 6.0'
+
+  pod 'ffmpeg-kit-ios-audio', '~> 6.0'
+
 end
 
 

+ 5 - 1
Podfile.lock

@@ -1,6 +1,7 @@
 PODS:
   - Alamofire (5.10.2)
   - BetterSegmentedControl (2.0.1)
+  - ffmpeg-kit-ios-audio (6.0)
   - IQKeyboardCore (1.0.5)
   - IQKeyboardManagerSwift (8.0.0):
     - IQKeyboardManagerSwift/Appearance (= 8.0.0)
@@ -62,6 +63,7 @@ PODS:
 DEPENDENCIES:
   - Alamofire
   - BetterSegmentedControl (~> 2.0)
+  - ffmpeg-kit-ios-audio (~> 6.0)
   - IQKeyboardManagerSwift
   - JXPagingView/Paging
   - JXSegmentedView
@@ -77,6 +79,7 @@ SPEC REPOS:
   trunk:
     - Alamofire
     - BetterSegmentedControl
+    - ffmpeg-kit-ios-audio
     - IQKeyboardCore
     - IQKeyboardManagerSwift
     - IQKeyboardNotification
@@ -102,6 +105,7 @@ EXTERNAL SOURCES:
 SPEC CHECKSUMS:
   Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
   BetterSegmentedControl: 09607b27861d49cbce48b7673b74f9150a3d371a
+  ffmpeg-kit-ios-audio: c013c6a68e822f7584f511931ad98e9a2040f23a
   IQKeyboardCore: 28c8bf3bcd8ba5aa1570b318cbc4da94b861711e
   IQKeyboardManagerSwift: 0c6fbbaa2e60739e48d7cf59f25661471a7a3a65
   IQKeyboardNotification: d7382c4466c5a5adef92c7452ebf861b36050088
@@ -121,6 +125,6 @@ SPEC CHECKSUMS:
   SwipeCellKit: 3972254a826da74609926daf59b08d6c72e619ea
   TSSmalCoacopods: 6aa97167f0c76b16fc7d1fd1eb198bb6aece4f68
 
-PODFILE CHECKSUM: 1ca24296fbc1745640e1f2f7038f00d9d3fcdb0f
+PODFILE CHECKSUM: 77fc358d4daf811a0f346e0934766d16c964f399
 
 COCOAPODS: 1.16.2