新增支持云端录音分享
authorliangzhenying <867174124@qq.com>
Wed, 12 Nov 2025 09:09:13 +0000 (17:09 +0800)
committerliangzhenying <867174124@qq.com>
Wed, 12 Nov 2025 09:09:13 +0000 (17:09 +0800)
22 files changed:
ShenYin/ShenYin/Api/Config.swift
ShenYin/ShenYin/Api/HttpClient2.swift
ShenYin/ShenYin/Cache/CacheManager.swift
ShenYin/ShenYin/Cache/ChatCache.swift
ShenYin/ShenYin/Cache/DCFolder.swift
ShenYin/ShenYin/Cache/FileSaver.swift [new file with mode: 0644]
ShenYin/ShenYin/Info.plist
ShenYin/ShenYin/UI/Home/CallingRecordVC.swift
ShenYin/ShenYin/UI/Home/Chat/ChatVC.swift
ShenYin/ShenYin/UI/Home/Device/HomeDeviceVC.swift
ShenYin/ShenYin/UI/Home/FieldRecording/FieldRecordingVC.swift
ShenYin/ShenYin/UI/Home/HistoryRecord/FileSaver.swift [deleted file]
ShenYin/ShenYin/UI/Home/HistoryRecord/VC/RecordShareVC.swift
ShenYin/ShenYin/UI/Home/RecordDetail/RecordDetailVC.swift
ShenYin/ShenYin/UI/Home/RecordTranslateVC.swift
ShenYin/ShenYin/UI/Launch/AppDelegate.swift
ShenYin/ShenYin/UI/Launch/AppManager.swift
ShenYin/ShenYin/UI/Launch/BleManager.swift
ShenYin/ShenYin/UI/Launch/JoySdkManager.swift
ShenYin/ShenYin/UI/Launch/SceneDelegate.swift
ShenYin/ShenYin/UI/Mine/Instructions/InstructionsVC.swift
ShenYin/ShenYin/lan/LanConfig.swift

index cf85a14..14aec59 100644 (file)
@@ -12,7 +12,8 @@ class DCConfig{
     static let wxAppID = "wxf19317f23b00d8ab"
     static let wxUniversalLink = "https://www.thisiot.com/flychat"
     static let wxSecret = "SECRET"
-    
+    /// 通用链接
+//    https://dc-cloud-pub.tos-cn-guangzhou.volces.com/apple-app-site-association
     //    static let appStoreLink = "https://itunes.apple.com/cn/app/6738022284?mt=8"
     static let appStoreLink = "https://itunes.apple.com/cn/app/6752551504?mt=8"
     
index d7d9818..f445efd 100644 (file)
@@ -200,7 +200,7 @@ extension CommomApi{
         case .uploadVoiceFile(_):
             return "/res/voice-file/upload"
         case .getProductList:
-            return "/dev/product/list?name=\(BleManager.shared.connectPeripheral?.device.name ?? "")&languageCode=\(DCConfig.appLanguage.lanCode)"
+            return "/dev/product/list?name=&languageCode=\(DCConfig.appLanguage.lanCode)"
         case .productBook(let productId):
             return "/res/prod-book-file?productId=\(productId)&languageCode=\(DCConfig.appLanguage.lanCode)"
         case .qaCategory:
index 24f4096..319c8fd 100644 (file)
@@ -36,7 +36,7 @@ extension CacheManager{
             return nil
         }
         
-        let filePath = DCFolder.getCacheDirectoryPath()! + "/\(Date().timeIntervalSince1970).png"
+        let filePath = DCFolder.cacheDirectory() + "/\(Date().timeIntervalSince1970).png"
         return saveToFile(data: data, filePath: filePath)
     }
     static func saveToFile(data:Data,filePath:String)->String?{
index 96e96aa..507e3c9 100644 (file)
@@ -21,11 +21,10 @@ public class ChatCellModel:NSObject,NSCoding  {
     }
     
     required public init?(coder aDecoder: NSCoder) {
-        type = Int(aDecoder.decodeObject(forKey: "type") as! String) ?? 0
+        type = Int(aDecoder.decodeObject(forKey: "type") as? String ?? "0") ?? 0
         content = aDecoder.decodeObject(forKey: "content") as? String ?? ""
-        id = Int(aDecoder.decodeObject(forKey: "id") as! String) ?? 0
+        id = Int(aDecoder.decodeObject(forKey: "id") as? String ?? "0") ?? 0
     }
-    
 }
 
 class ChatCache {
@@ -33,7 +32,9 @@ class ChatCache {
     private let userDefaults = UserDefaults.standard
     
     func saveList(_ list: [ChatCellModel]) {
-        var fullList = get()
+//        var fullList = get()
+//        fullList.append(contentsOf: list)
+        var fullList = [ChatCellModel]()
         fullList.append(contentsOf: list)
         if fullList.count > 1000{
             fullList = Array(fullList.dropFirst(fullList.count - 1000))
@@ -49,11 +50,11 @@ class ChatCache {
     }
     
     func get() -> [ChatCellModel] {
-        let data = YLToos.readFile(from:  DCFolder.chatCache)
-        if data == nil{
+        
+        guard let data = YLToos.readFile(from:  DCFolder.chatCache) else{
             return []
         }
-        if let deviceList = NSKeyedUnarchiver.unarchiveObject(with: data!) as? [ChatCellModel] {
+        if let deviceList = NSKeyedUnarchiver.unarchiveObject(with: data) as? [ChatCellModel] {
             return deviceList
         }
         return []
index 376b999..0794ea6 100644 (file)
@@ -18,25 +18,29 @@ class DCFolder {
         return path
     }
     
-    static func createFolder(_ foldPath:String){
+    @discardableResult
+    static func createFolder(_ foldPath:String)->Bool{
         let fileManager = FileManager.default
         if !fileManager.fileExists(atPath: foldPath) {
             do {
                 try FileManager.default.createDirectory(atPath: foldPath, withIntermediateDirectories: true, attributes: nil)
+                return true
             } catch {
                 Logger.debug("创建文件夹失败!!!",foldPath)
+                return false
             }
         }
+        return true
     }
     
     // 获取应用沙盒中的缓存目录路径
-    static func getCacheDirectoryPath() -> String? {
+    static func cacheDirectory() -> String! {
         // 获取缓存目录的URL
         if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
             // 转换为路径字符串
             return cacheURL.path
         }
-        return nil
+        return ""
     }
     
     static func moveFile(srcPath:String,desPath:String)->Bool{
@@ -49,6 +53,22 @@ class DCFolder {
             return false
         }
     }
+    @discardableResult
+    static func copy(srcPath:String,desPath:String)->Bool{
+        let fileManager = FileManager.default
+        
+        do {
+            if fileManager.fileExists(atPath: desPath) {
+                try fileManager.removeItem(atPath: desPath)
+            }
+            try fileManager.copyItem(atPath: srcPath, toPath: desPath)
+            return true
+        } catch {
+            Logger.debug("拷贝文件失败: \(error)")
+            return false
+        }
+    }
+    
     static func recordFileResave(srcPath:String)->String?{
         let desPath = DCFolder.record + (srcPath as NSString).lastPathComponent
         let fileManager = FileManager.default
diff --git a/ShenYin/ShenYin/Cache/FileSaver.swift b/ShenYin/ShenYin/Cache/FileSaver.swift
new file mode 100644 (file)
index 0000000..3ccab0a
--- /dev/null
@@ -0,0 +1,164 @@
+//
+//  FileSaver.swift
+//  ShenYin
+//
+//  Created by liangshi on 2025/10/27.
+//
+
+import ZipArchive
+
+class FileSaver {
+    /// 获取文档目录的URL
+    private static func getDocumentsDirectory() -> String {
+        // 获取缓存目录的URL
+        if let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
+            // 转换为路径字符串
+            return cacheURL.path
+        }
+        return ""
+    }
+    
+    /// 将字符串保存为txt文件
+    /// - Parameters:
+    ///   - string: 要保存的字符串
+    ///   - fileName: 文件名(不包含扩展名)
+    /// - Returns: 是否保存成功
+    static func saveStringAsTxt(_ string: String,fileName:String = "\(Date().format(.full6))") -> String? {
+        let fileURL = getDocumentsDirectory() + "/\(fileName)"
+        do {
+            try string.write(to: URL(fileURLWithPath: fileURL), atomically: true, encoding:.utf8)
+            return fileURL
+        } catch {
+            Logger.debug("保存文件失败: \(error)")
+            return nil
+        }
+    }
+    static func createFolder(_ foldPath:String)->Bool{
+        let fileManager = FileManager.default
+        if fileManager.fileExists(atPath: foldPath) {
+            Logger.debug("文件夹已经存在,先移除",foldPath)
+            do {
+                try  FileManager.default.removeItem(atPath: foldPath)
+            } catch {
+                Logger.debug("移除文件夹失败!!!",foldPath)
+                return false
+            }
+        }
+        do {
+            try FileManager.default.createDirectory(atPath: foldPath, withIntermediateDirectories: true, attributes: nil)
+            return true
+        } catch {
+            Logger.debug("创建文件夹失败!!!",foldPath)
+            return false
+        }
+    }
+}
+
+
+extension FileSaver{
+    static func zipFiles(files: [String]) -> String? {
+        let zipArchive = ZipArchive()
+        let des = getDocumentsDirectory() + "/record.zip"
+        if zipArchive.createZipFile2(des) {
+            for fileURL in files {
+                
+                if !zipArchive.addFile(toZip: fileURL, newname: URL(fileURLWithPath: fileURL).lastPathComponent ) {
+                    if !FileManager.default.fileExists(atPath: fileURL){
+                        Logger.debug("文件不存在",fileURL)
+                    }
+                    Logger.debug("Failed to add file: \(fileURL)")
+                    zipArchive.closeZipFile2()
+                    return nil
+                }
+            }
+            zipArchive.closeZipFile2()
+            Logger.debug("\(des)")
+            return des
+        } else {
+            Logger.debug("Failed to create zip file")
+            return nil
+        }
+    }
+    static func creatFolder(folderName:String,files: [String]) -> String? {
+        let folder1 = DCFolder.cacheDirectory() + "/\(folderName)"
+        if !createFolder(folder1){
+            return nil
+        }
+        for src in files {
+            let des = folder1 + "/\( URL(fileURLWithPath: src).lastPathComponent)"
+            DCFolder.copy(srcPath: src, desPath: des)
+        }
+        
+        return folder1
+    }
+    static func zipFolder(paths:[String])->String?{
+//        let zipPath = DCFolder.cacheDirectory() + "/record.zip"
+//        let zipArchive = ZipArchive()
+//        
+//        if zipArchive.createZipFile2(zipPath) {
+//            for path in paths {
+//                let des = "\( URL(fileURLWithPath: path).lastPathComponent)"
+//                Logger.debug("文件夹",path)
+//                Logger.debug("新文件夹",des)
+//                if zipArchive.addFile(toZip: path, newname: des){
+//                    Logger.debug("压缩失败",path)
+//                }
+//                
+//            }
+//            zipArchive.closeZipFile2()
+//            Logger.debug("压缩成功",zipPath)
+//            return zipPath
+//        } else {
+//            Logger.debug("创建压缩文件失败",zipPath)
+//            return nil
+//        }
+        
+        let zipPath = zipFolders(folderPaths: paths)
+        return zipPath
+    }
+    
+    
+    static func zipFolders(folderPaths: [String]) -> String? {
+        // 获取缓存目录路径
+        guard let cacheDirectory = FileManager.default.urls(for:.cachesDirectory, in:.userDomainMask).first else {
+            print("无法获取缓存目录")
+            return nil
+        }
+        // 构建压缩文件路径
+        let zipFilePath = cacheDirectory.appendingPathComponent("record.zip").path
+        
+        let zipArchive = ZipArchive()
+        // 创建zip文件
+        if zipArchive.createZipFile2(zipFilePath) {
+            for folderPath in folderPaths {
+                addFilesFromFolder(folder: folderPath, zipArchive: zipArchive, prefix: folderPath.components(separatedBy: "/").last! + "/")
+            }
+            zipArchive.closeZipFile2()
+            return zipFilePath
+        } else {
+            print("创建zip文件失败")
+            return nil
+        }
+    }
+    
+    private static func addFilesFromFolder(folder: String, zipArchive: ZipArchive, prefix: String) {
+        guard let folderURL = URL(string: folder) else {
+            print("无效的文件夹路径: \(folder)")
+            return
+        }
+        let fileManager = FileManager.default
+        do {
+            let files = try fileManager.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil)
+            for file in files {
+                let filePath = folderURL.appendingPathComponent(file.lastPathComponent).path
+                let newName = prefix + file.lastPathComponent
+                if !zipArchive.addFile(toZip: filePath, newname: newName) {
+                    print("添加文件 \(filePath) 到zip失败")
+                }
+            }
+        } catch {
+            print("读取文件夹 \(folder) 失败: \(error)")
+        }
+    }
+}
+
index b0ef8fa..2c22161 100644 (file)
@@ -53,5 +53,7 @@
        </array>
        <key>NSLocationWhenInUseUsageDescription</key>
        <string>允许APP使用定位方便查找耳机</string>
+       <key>ITSAppUsesNonExemptEncryption</key>
+       <false/>
 </dict>
 </plist>
index 9f406d2..4148fc5 100644 (file)
@@ -149,7 +149,7 @@ extension CallingRecordVC:FieldRecordBarDelegate{
                     model.duration = YLToos.getDuration(model.path)
                     model.origin = textView.text
                     model.translate = ""
-                    
+                    App.shared.popPage(animated: false)
                     App.shared.goToPage(RecordDetailVC(record: model))
                 }
             }
index 2b3d9b5..1ee36a5 100644 (file)
@@ -15,9 +15,9 @@ class ChatVC:BaseVC{
     let toolBar = ChatRecordBar()
     let tableView = UITableView(frame: CGRectZero, style: .plain)
     let headerView = ChatHeaderView(frame: CGRect(x: 0, y: 0, width: kScreenWid, height: 260))
-    var dataList:[ChatCellModel] = []
+    
     var roleList:[JoyGemSpeaker] = []
-    var isTtsPlaying = false
+    
     
     deinit {
         PcmSound.sharedInstance().stopAudioUnit()
@@ -72,29 +72,26 @@ class ChatVC:BaseVC{
         }
     }
     override func setupData() {
-        JoyGemChatManager.shared().delegate = self
-        PcmSound.sharedInstance().delegate = self
         JoyGemChatManager.shared().addObserver(self, forKeyPath: "isActive",options: [.new, .old], context: nil);
-        
-        
-//        // 创建一个自定义的串行队列
-//        let customQueue = DispatchQueue(label: "com.example.customQueue")
-//        customQueue.async {
-//            // 这里的代码在子线程中执行
-//            print("This is running on a background thread. Thread: \(Thread.current)")
-//            // 模拟一些耗时操作
-//            Thread.sleep(forTimeInterval: 2)
-//            DispatchQueue.main.async {
-//                // 回到主线程更新UI等操作
-//                print("Back on the main thread. Thread: \(Thread.current)")
-//            }
-//        }
-        YLprogressHUD.showLoading()
-        let list = ChatCache.shared.get()
-        dataList.append(contentsOf: list)
+        JoySdkManager.shared.chatStateChange = {[weak self] code in
+            guard let self = self else { return  }
+            switch code {
+            case .startRecord:
+                toolBar.isRecord = true
+                break
+            case .stopRecord:
+                toolBar.isRecord = false
+                break
+            default:
+                break
+            }
+        }
+        JoySdkManager.shared.chatNeedReloadData = {[weak self] in
+            guard let self = self else { return  }
+            refreshData()
+        }
+
         refreshData()
-        YLprogressHUD.hidden()
-        
         loadRoleList()
     }
     override func viewWillDisappear(_ animated: Bool) {
@@ -102,8 +99,7 @@ class ChatVC:BaseVC{
         
         ///结束对话,但并不会终止正在播放的录音
         JoyGemChatManager.shared().stopChat()
-        
-        ChatCache.shared.saveList(dataList)
+        ChatCache.shared.saveList(JoySdkManager.shared.dataList)
     }
     override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)
@@ -139,8 +135,8 @@ class ChatVC:BaseVC{
     }
     func refreshData(){
         tableView.reloadData()
-        if dataList.count > 0 {
-            tableView.scrollToRow(at: IndexPath(row: dataList.count - 1, section: 0), at: .none, animated: true)
+        if JoySdkManager.shared.dataList.count > 0 {
+            tableView.scrollToRow(at: IndexPath(row: JoySdkManager.shared.dataList.count - 1, section: 0), at: .none, animated: true)
         }
     }
     func loadRoleList(){
@@ -180,7 +176,7 @@ extension ChatVC:ChatRecordBarDelegate{
         model.type = 1
         model.content = ""
         model.id = -1
-        dataList.append(model)
+        JoySdkManager.shared.dataList.append(model)
         refreshData()
         
         JoyGemChatManager.shared().stopChat()
@@ -194,12 +190,12 @@ extension ChatVC:ChatRecordBarDelegate{
 
 extension ChatVC:UITableViewDelegate,UITableViewDataSource {
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-        return dataList.count
+        return JoySdkManager.shared.dataList.count
     }
     
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let cell = ChatCell.cell(withTableView: tableView, indexPath: indexPath)
-        cell.data = dataList[indexPath.row]
+        cell.data = JoySdkManager.shared.dataList[indexPath.row]
         return cell
     }
     func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@@ -207,104 +203,4 @@ extension ChatVC:UITableViewDelegate,UITableViewDataSource {
     }
 }
 
-extension ChatVC : JoyGemChatManagerDelegate {
-    func chatManager(_ manager: JoyGemChatManager, audioData data: Data?, index: UInt, sampleRate: UInt, finish isFinish: Bool) {
-        if (!isTtsPlaying) {
-            isTtsPlaying = true
-            Logger.debug("开始播放")
-            JoyGemChatManager.shared().playAudioStart()
-        }
-        Logger.debug("收到音频数据", data?.count ?? 0)
-        PcmSound.sharedInstance().feed(data)
-    }
-    
-    func chatManager(_ manager: JoyGemChatManager, stateChange code: JoyGemStateCode) {
-        Logger.debug("事件 ",code)
-        switch code {
-        case .startRecord:
-            toolBar.isRecord = true
-            if (isTtsPlaying) {
-                isTtsPlaying = false
-                PcmSound.sharedInstance().cleanup()
-                PcmSound.sharedInstance().setup(16000, withchannels: 1)
-                let options: AVAudioSession.CategoryOptions = [.allowBluetooth]
-                do {
-                    try AVAudioSession.sharedInstance().setCategory(.playback, options: [options])
-                    try AVAudioSession.sharedInstance().setActive(true)
-                } catch {
-                    Logger.debug(error)
-                }
-            }
-//            stateLabel.text = "正在录音"
-            break
-        case .stopRecord:
-            toolBar.isRecord = false
-            break
-        default:
-            break
-        }
-    }
-    func chatManager(_ manager: JoyGemChatManager, eventUpdate event: JoyGemChatEvent) {
-        Logger.debug("事件改变",event)
-    }
-    
-    func chatManager(_ manager: JoyGemChatManager, onError code: JoyGemErrorCode) {
-        Logger.debug("收到错误",code)
-    }
-    
-    func chatManager(_ manager: JoyGemChatManager, audioData data: Data?, finish isFinish: Bool) {
-
-    }
-    
-    func chatManager(_ manager: JoyGemChatManager, requestText text: String?, index: UInt) {
-        Logger.debug(text,index)
-        guard let text = text else{
-            return
-        }
-        if text.isEmpty {
-            return
-        }
-        if dataList.last?.id == -1 {
-            dataList.removeLast()
-        }
-        if let model = dataList.last,model.id == index,model.type == 1 {
-            model.content = text
-        }
-        else{
-            let model = ChatCellModel()
-            model.type = 1
-            model.content = text
-            model.id = Int(index)
-            dataList.append(model)
-        }
-        refreshData()
-    }
-    
-    func chatManager(_ manager: JoyGemChatManager, responseText text: String?, index: UInt, finish isFinish: Bool) {
-//        responseTextView.text = text;
-        Logger.debug(text,index)
-        if let text = text {
-            if let model = dataList.last,model.id == index,model.type == 0 {
-                model.content = text
-            }
-            else{
-                let model = ChatCellModel()
-                model.type = 0
-                model.content = text
-                model.id = Int(index)
-                dataList.append(model)
-            }
-            refreshData()
-        }
-    }
-}
 
-extension ChatVC : PcmSoundDelegate {
-    
-    func remaining(_ frames: UInt) {
-        Logger.debug("[frame:\(frames)]");
-        if (frames == 0) {
-            JoyGemChatManager.shared().playAudioEnd()
-        }
-    }
-}
index 37e6f7e..8446e2e 100644 (file)
@@ -55,6 +55,7 @@ class HomeDeviceVC:BaseVC{
                 for item in DeviceVM.shared.dataList {
                     if item.mac ==  lastMac{
                         self?.startConectBle(item)
+                        return
                     }
                 }
             }
index 7d427c6..551c1c2 100644 (file)
@@ -54,8 +54,6 @@ class FieldRecordingVC:BaseVC{
     }
     override func setupData() {
         JoyGemRecordManager.shared().delegate = self
-        PcmSound.sharedInstance().delegate = self;
-        PcmSound.sharedInstance().setup(16000, withchannels: 1)
         let options: AVAudioSession.CategoryOptions = [.allowBluetooth]
         do {
             try AVAudioSession.sharedInstance().setCategory(.playback, options: [options])
@@ -81,8 +79,6 @@ class FieldRecordingVC:BaseVC{
             CustomAlert.showWith(title: "提示", messgage: "当前正在录音,确定放弃当前录音吗?".l10n, cancelCallback: {}) {[unowned self] in
                 self.rhythView.stopTimer()
                 JoyGemRecordManager.shared().stop()
-                PcmSound.sharedInstance().stopAudioUnit()
-                PcmSound.sharedInstance().cleanup()
                 self.navigationController?.popViewController(animated: true)
             }
         }
@@ -146,7 +142,7 @@ extension FieldRecordingVC:FieldRecordBarDelegate{
                     model.duration = YLToos.getDuration(model.path)
                     model.origin = textView.text
                     model.translate = ""
-
+                    App.shared.popPage(animated: false)
                     App.shared.goToPage(RecordDetailVC(record: model))
                 }
             }
@@ -157,7 +153,7 @@ extension FieldRecordingVC:FieldRecordBarDelegate{
 extension FieldRecordingVC : JoyGemRecordManagerDelegate {
     
     func recordManager(_ manager: JoyGemRecordManager, didUpdateAudioData data: Data) {
-        PcmSound.sharedInstance().feed(data)
+//        PcmSound.sharedInstance().feed(data)
     }
     
     func recordManager(_ manager: JoyGemRecordManager, didUpdateTranslate model: JoyGemAsrModel) {
@@ -203,14 +199,6 @@ extension FieldRecordingVC : JoyGemRecordManagerDelegate {
         if (code == .actively) {
 //            YLprogressHUD.showAutoHiden("用户主动结束")
 //            toolBar.recordBtn.stopLoading()
-//            let v = PopTextField()
-//            v.show()
-//            v.cancelTap = {
-//                
-//            }
-//            v.okTap = {
-//                
-//            }
         } else {
 //            YLprogressHUD.showAutoHiden("自动结束")
             ///判断当前蓝牙是否连接从而确定是否因为蓝牙断开造成的录音断开
@@ -221,10 +209,3 @@ extension FieldRecordingVC : JoyGemRecordManagerDelegate {
         Logger.debug("onError: \(code)")
     }
 }
-
-extension FieldRecordingVC : PcmSoundDelegate {
-    
-    func remaining(_ frames: UInt) {
-        Logger.debug("[frame:\(frames)]");
-    }
-}
diff --git a/ShenYin/ShenYin/UI/Home/HistoryRecord/FileSaver.swift b/ShenYin/ShenYin/UI/Home/HistoryRecord/FileSaver.swift
deleted file mode 100644 (file)
index 25cf978..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-//
-//  FileSaver.swift
-//  ShenYin
-//
-//  Created by liangshi on 2025/10/27.
-//
-
-import ZipArchive
-
-class FileSaver {
-    /// 获取文档目录的URL
-    private static func getDocumentsDirectory() -> URL {
-        let paths = FileManager.default.urls(for:.cachesDirectory, in:.userDomainMask)
-        return paths[0]
-    }
-    
-    /// 将字符串保存为txt文件
-    /// - Parameters:
-    ///   - string: 要保存的字符串
-    ///   - fileName: 文件名(不包含扩展名)
-    /// - Returns: 是否保存成功
-    static func saveStringAsTxt(_ string: String,fileName:String = "\(Date().format(.full6))") -> String? {
-        let fileURL = getDocumentsDirectory().appendingPathComponent(fileName).appendingPathExtension("txt")
-        do {
-            try string.write(to: fileURL, atomically: true, encoding:.utf8)
-            return fileURL.path
-        } catch {
-            Logger.debug("Error saving file: \(error)")
-            return nil
-        }
-    }
-
-}
-
-
-extension FileSaver{
-    static func zipFiles(files: [String]) -> String? {
-        let zipArchive = ZipArchive()
-        let des = getDocumentsDirectory().appendingPathComponent("\(Date().format(.full5))").appendingPathExtension("zip").path
-        if zipArchive.createZipFile2(des) {
-            for fileURL in files {
-                
-                if !zipArchive.addFile(toZip: fileURL, newname: URL(fileURLWithPath: fileURL).lastPathComponent ) {
-                    if !FileManager.default.fileExists(atPath: fileURL){
-                        Logger.debug("文件不存在",fileURL)
-                    }
-                    Logger.debug("Failed to add file: \(fileURL)")
-                        zipArchive.closeZipFile2()
-                        return nil
-                    }
-            }
-            zipArchive.closeZipFile2()
-            Logger.debug("\(des)")
-            return des
-        } else {
-            Logger.debug("Failed to create zip file")
-            return nil
-        }
-    }
-}
-
index bb19919..cd43b28 100644 (file)
@@ -59,80 +59,152 @@ class RecordShareVC: BaseVC{
     override func setupData() {
 
     }
-    func loadFile(filedId id:String){
-        let api = CommomApi.recordUrl(filedId: id)
-        HttpClient.request(api) {[weak self] (model:CommonDataModel,res) in
-            guard let self = self else {return}
-            if model.isSuccess{
-                startDownload(url: model.data)
+
+    
+    @objc func btnAction(){
+        if recordList.count > 0 {
+           startShare()
+        }
+        else{
+            YLprogressHUD.showLoading()
+            loadDetails {[weak self] result in
+                guard let self = self else{return}
+                if result{
+                    YLprogressHUD.hidden()
+                    startShare()
+                }
+                else{
+                    YLprogressHUD.showAutoHiden("请求失败".l10n)
+                }
             }
         }
     }
-    func startDownload(url:String){
-        //        let url = "https://dc-cloud-priv.tos-cn-guangzhou.volces.com/music-file/2025_10_09/1976135290430767105.mp3"
-//        HttpClient.download(url: url,type: "wav"){[weak self] result in
-//            guard let self = self else {return}
-//            if let path = result{
-//                recordModel.path = path
-//            }
-//        }
-    }
     
-    @objc func btnAction(){
-        if recordList.count == 1,let recordModel = recordList.first {
-            if currentType == .audio {
+    func startShare(){
+        var folders = [String]()
+        for recordModel in recordList {
+            var list = [String]()
+            
+            if currentType == .audio_document || currentType == .audio{
                 if recordModel.path.isNotEmpty {
-                    saveFile(filePath: recordModel.path)
+                    let des = DCFolder.cacheDirectory() + "/audio.wav"
+                    if DCFolder.copy(srcPath: recordModel.path, desPath: des) {
+                        list.append(des)
+                    }
                 }
             }
-            else if currentType == .document{
-                guard let path = FileSaver.saveStringAsTxt(recordModel.origin,fileName: recordModel.name + "") else {
-                    return
+            
+            if currentType == .document || currentType == .audio_document{
+                if let orginPath = FileSaver.saveStringAsTxt(recordModel.origin ,fileName: "original.txt") {
+                    list.append(orginPath)
                 }
-                saveFile(filePath: path)
-            }
-            else{
-                
-                guard let orginPath = FileSaver.saveStringAsTxt(recordModel.origin ,fileName: recordModel.name + "") else {
-                    return
-                }
-                var list = [orginPath]
-                if recordModel.path.isNotEmpty {
-                    list.append(recordModel.path)
-                }
-                
-                if recordModel.translate.count > 0,let path = FileSaver.saveStringAsTxt(recordModel.translate ,fileName: recordModel.name + "_translate") {
+                if recordModel.translate.count > 0,let path = FileSaver.saveStringAsTxt(recordModel.translate ,fileName: "translate.txt") {
                     list.append(path)
                 }
-                if recordModel.summary.count > 0,let path = FileSaver.saveStringAsTxt(recordModel.summary ,fileName: recordModel.name + "_summary") {
+                if recordModel.summary.count > 0,let path = FileSaver.saveStringAsTxt(recordModel.summary ,fileName: "summary.txt") {
                     list.append(path)
                 }
-                guard let path = FileSaver.zipFiles(files: list) else { return  }
-                
-                saveFile(filePath: path)
             }
+            
+            
+            if let folder = FileSaver.creatFolder(folderName: recordModel.name + "_" + recordModel.createTime.components(separatedBy: CharacterSet(charactersIn: " -/")).joined(), files: list){
+                folders.append(folder)
+            }
+            Logger.debug(list)
+        }
+        Logger.debug(folders)
+        if let path = FileSaver.zipFolder(paths: folders){
+            //                saveFile(filePath: path)
+            let vc = UIActivityViewController(activityItems: [URL(fileURLWithPath: path)], applicationActivities: nil)
+            vc.modalPresentationStyle = .overFullScreen
+            App.shared.topVC().present(vc, animated: true)
         }
     }
 }
-extension RecordShareVC:UIDocumentPickerDelegate{
-    //保存文件到手机文件指定目录
-    func saveFile(filePath:String) {
-        if !FileManager.default.fileExists(atPath: filePath){
-            Logger.debug("文件不存在",filePath)
-            return
+extension RecordShareVC{
+    func loadDetails(_ finished:@escaping ((Bool) -> Void)){
+        recordList.removeAll()
+        let dispatchGroup = DispatchGroup()
+        for item in cloudList {
+            dispatchGroup.enter()
+            requestDetail(item) {[weak self] model in
+                guard let self = self else { return  }
+                if let data = model{
+                    recordList.append(data)
+                }
+                dispatchGroup.leave()
+            }
         }
         
-        let fUrl = NSURL(fileURLWithPath: filePath)
-        let vc = UIDocumentPickerViewController(url: fUrl as URL, in: .exportToService);
-        vc.delegate = self
-        vc.modalPresentationStyle = .overFullScreen
-        self.present(vc, animated: true)
+        dispatchGroup.notify(queue:.main) {
+            finished(true)
+        }
     }
-    public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
-        controller.dismiss(animated: true)
+    
+    func requestDetail(_ remoteData:RecordHistoryModel,finished:@escaping ((RecordDetailModel?) -> Void)){
+        let api = CommomApi.getVoiceRecord(recordId: remoteData.id)
+        HttpClient.request(api) {[weak self](result:RecordHistoryDetailRoot,res) in
+            if result.isSuccess{
+                let data = result.data
+                let model = RecordDetailModel()
+                model.type = data.type
+                model.origin = data.summary
+                model.translate = ""
+                model.path = ""
+                model.name = data.name
+                model.createTime = data.createTime
+                self?.requestVoiceRecordText(remoteModel: remoteData, localModel: model,finished: finished)
+            }
+            else{
+                finished(nil)
+            }
+        }
+    }
+    func requestVoiceRecordText(remoteModel:RecordHistoryModel,localModel:RecordDetailModel,finished:@escaping ((RecordDetailModel?) -> Void)){
+        var params:[String:Any] = [:]
+        params["recordId"] = remoteModel.id
+        params["page"] = 1
+        let api = CommomApi.getVoiceRecordText(param: params)
+        HttpClient.request(api) {[weak self](result:CloudRecordDetailRoot,res) in
+            if result.isSuccess,let model = result.data.list.first{
+                localModel.origin = model.text
+                localModel.translate = model.translateText
+                localModel.from = AiSupportLanguage(rawValue: model.sourceLanguage) ?? .chinse
+                localModel.to = AiSupportLanguage(rawValue: model.targetLanguage) ?? .english
+                
+                self?.getRecordPath(filedId: remoteModel.fileId) {  result in
+                    if let path = result{
+                        localModel.path = path
+                    }
+                    finished(localModel)
+                }
+                
+            }
+            else{
+                finished(nil)
+            }
+        }
+    }
+    func getRecordPath(filedId id:String,finished:@escaping ((String?) -> Void)){
+        let api = CommomApi.recordUrl(filedId: id)
+        HttpClient.request(api) {[weak self] (model:CommonDataModel,res) in
+            guard let self = self else {return}
+            if model.isSuccess{
+                startDownload(url: model.data,finished: finished)
+            }
+            else{
+                finished(nil)
+            }
+        }
+    }
+    func startDownload(url:String,finished:@escaping ((String?) -> Void)){
+        HttpClient.download(url: url,type: "wav"){result in
+            finished(result)
+        }
     }
 }
 
+
 extension RecordShareVC:UITableViewDelegate,UITableViewDataSource {
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         return list.count
index 51c02c6..35aef70 100644 (file)
@@ -49,14 +49,15 @@ class RecordDetailVC:BaseVC{
         translateView.textView.font = FontCache.getFont().font
         summaryView.textView.font = FontCache.getFont().font
     }
-    override func viewDidAppear(_ animated: Bool) {
-        super.viewDidAppear(animated)
-        if needFrontVC == false {
-            var list = self.navigationController!.viewControllers
-            list.remove(at: list.count - 2)
-            self.navigationController!.viewControllers = list
-        }
-    }
+    
+//    override func viewDidAppear(_ animated: Bool) {
+//        super.viewDidAppear(animated)
+//        if needFrontVC == false {
+//            var list = self.navigationController!.viewControllers
+//            list.remove(at: list.count - 2)
+//            self.navigationController!.viewControllers = list
+//        }
+//    }
     override func viewWillDisappear(_ animated: Bool) {
         super.viewWillDisappear(animated)
         if remoteData == nil,let localData = data {
index 601aea9..60f0451 100644 (file)
@@ -157,6 +157,7 @@ extension RecordTranslateVC:RecordBarDelegate{
                     model.duration = YLToos.getDuration(model.path)
                     model.origin = textView.text ?? ""
                     model.translate = translateTextView.text ?? ""
+                    App.shared.popPage(animated: false)
                     App.shared.goToPage(RecordDetailVC(record: model))
                 }
             }
index 6b89563..75c219d 100644 (file)
@@ -50,16 +50,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 //        Logger.verbose("注册远程通知发生错误:", error)
 //    }
 //    
-//    func applicationDidEnterBackground(_ application: UIApplication) {
-//        DB.shared.save()
-//        JPush.shared.tellAppState(0)
-//        Logger.verbose("App 进入后台")
-//    }
-//    
-//    func applicationWillEnterForeground(_ application: UIApplication) {
-//        JPush.shared.tellAppState(1)
-//        Logger.verbose("App 将要进入前台")
-//    }
+
 //    
 //    private var previousRefreshTime: TimeInterval = 0.0
 //    func applicationDidBecomeActive(_ application: UIApplication) {
index 24dcdc2..29be998 100644 (file)
@@ -8,6 +8,8 @@
 class App{
     static let shared = App()
     var window:UIWindow?
+    var isInBackground = false
+
     
     func topVC()->UIViewController!{
         guard let vc = window?.rootViewController else{
@@ -22,6 +24,7 @@ class App{
         Logger.debug(DCFolder.user)
         Logger.debug(DCFolder.record)
         window?.rootViewController = BaseTC()
+        JoySdkManager.shared.loadChatCache()
     }
     func goToLogin(){
         window?.rootViewController = BaseNC(rootViewController: LoginVC())
index e80f359..e38eefe 100644 (file)
@@ -272,19 +272,29 @@ extension BleManager : JoyGemAIManagerDelegate {
             }
         case .recordLive:
             Logger.debug("请求开启现场录音")
+            if App.shared.isInBackground {
+                return
+            }
 //            YLprogressHUD.showAutoHiden("请求开启现场录音")
-            if !App.shared.topVC().isKind(of: FieldRecordingVC.self) {
+            if let vc = App.shared.topVC() as? FieldRecordingVC {
+                vc.startRecord()
+            }
+            else{
                 App.shared.goToPage(FieldRecordingVC(autoStart: true))
             }
         case .recordCall:
                 Logger.debug("请求开启通话录音")
+            if App.shared.isInBackground {
+                return
+            }
 //            YLprogressHUD.showAutoHiden("请求开启通话录音")
-            if !App.shared.topVC().isKind(of: CallingRecordVC.self) {
+            if let vc = App.shared.topVC() as? CallingRecordVC {
+                vc.startRecord()
+            }else{
                 App.shared.goToPage(CallingRecordVC(autoStart: true))
             }
         case .si:
                 Logger.debug( "请求开启同传听译")
-//            YLprogressHUD.showAutoHiden( "请求开启同传听译")
         case .chat:
             Logger.debug("通知开启了AI对话")
 //            YLprogressHUD.showAutoHiden( "通知开启了AI对话")
index d8dade2..9271311 100644 (file)
@@ -12,11 +12,17 @@ import AVFoundation
 class JoySdkManager:NSObject{
     static let shared = JoySdkManager()
     
+    var isTtsPlaying = false
+    var dataList:[ChatCellModel] = []
+    var chatStateChange:((JoyGemStateCode)->Void)?
+    var chatNeedReloadData:(()->Void)?
+}
+extension JoySdkManager {
     func setupSdk(){
         //MARK: 初始化SDK
         JoyGemAIManager.shared().initSdk()
         //MARK: 配置日志类型
-//        JoyGemAIManager.shared().setLogMode(JoyGemLogMode.all)
+        //        JoyGemAIManager.shared().setLogMode(JoyGemLogMode.all)
         JoyGemAIManager.shared().setLogMode(JoyGemLogMode.close)
         
         let fileManager = FileManager.default
@@ -65,6 +71,112 @@ class JoySdkManager:NSObject{
         let tempPath = NSTemporaryDirectory();
         JoyGemAIManager.shared().setCacheDirectory(tempPath)
         JoyGemAIManager.shared().setTWSAutoSync(true)
+        
+        JoyGemChatManager.shared().delegate = self
     }
 }
 
+
+extension JoySdkManager : JoyGemChatManagerDelegate {
+    func loadChatCache(){
+        dataList = ChatCache.shared.get()
+    }
+    
+    func chatManager(_ manager: JoyGemChatManager, audioData data: Data?, index: UInt, sampleRate: UInt, finish isFinish: Bool) {
+        PcmSound.sharedInstance().delegate = self
+        if (!isTtsPlaying) {
+            isTtsPlaying = true
+            Logger.debug("开始播放")
+            JoyGemChatManager.shared().playAudioStart()
+        }
+        Logger.debug("收到音频数据", data?.count ?? 0)
+        PcmSound.sharedInstance().feed(data)
+    }
+    
+    func chatManager(_ manager: JoyGemChatManager, stateChange code: JoyGemStateCode) {
+        chatStateChange?(code)
+        Logger.debug("事件 ",code)
+        switch code {
+        case .startRecord:
+            if (isTtsPlaying) {
+                isTtsPlaying = false
+                PcmSound.sharedInstance().cleanup()
+                PcmSound.sharedInstance().setup(16000, withchannels: 1)
+                let options: AVAudioSession.CategoryOptions = [.allowBluetooth]
+                do {
+                    try AVAudioSession.sharedInstance().setCategory(.playback, options: [options])
+                    try AVAudioSession.sharedInstance().setActive(true)
+                } catch {
+                    Logger.debug(error)
+                }
+            }
+            //            stateLabel.text = "正在录音"
+            break
+        case .stopRecord:
+            break
+        default:
+            break
+        }
+    }
+    func chatManager(_ manager: JoyGemChatManager, eventUpdate event: JoyGemChatEvent) {
+        Logger.debug("事件改变",event)
+    }
+    
+    func chatManager(_ manager: JoyGemChatManager, onError code: JoyGemErrorCode) {
+        Logger.debug("收到错误",code)
+    }
+    
+    func chatManager(_ manager: JoyGemChatManager, audioData data: Data?, finish isFinish: Bool) {
+        
+    }
+    
+    func chatManager(_ manager: JoyGemChatManager, requestText text: String?, index: UInt) {
+        Logger.debug(text,index)
+        guard let text = text else{
+            return
+        }
+        if text.isEmpty {
+            return
+        }
+        if dataList.last?.id == -1 {
+            dataList.removeLast()
+        }
+        if let model = dataList.last,model.id == index,model.type == 1 {
+            model.content = text
+        }
+        else{
+            let model = ChatCellModel()
+            model.type = 1
+            model.content = text
+            model.id = Int(index)
+            dataList.append(model)
+        }
+        chatNeedReloadData?()
+    }
+    
+    func chatManager(_ manager: JoyGemChatManager, responseText text: String?, index: UInt, finish isFinish: Bool) {
+        Logger.debug(text,index)
+        if let text = text {
+            if let model = dataList.last,model.id == index,model.type == 0 {
+                model.content = text
+            }
+            else{
+                let model = ChatCellModel()
+                model.type = 0
+                model.content = text
+                model.id = Int(index)
+                dataList.append(model)
+            }
+            chatNeedReloadData?()
+        }
+    }
+}
+extension JoySdkManager : PcmSoundDelegate {
+    
+    func remaining(_ frames: UInt) {
+        Logger.debug("[frame:\(frames)]");
+        if (frames == 0) {
+            JoyGemChatManager.shared().playAudioEnd()
+        }
+    }
+}
index de56146..5b24ec6 100644 (file)
@@ -32,6 +32,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
     func sceneDidBecomeActive(_ scene: UIScene) {
         // Called when the scene has moved from an inactive state to an active state.
         // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
+        App.shared.isInBackground = false
     }
 
     func sceneWillResignActive(_ scene: UIScene) {
@@ -48,6 +49,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         // Called as the scene transitions from the foreground to the background.
         // Use this method to save data, release shared resources, and store enough scene-specific state information
         // to restore the scene back to its current state.
+        App.shared.isInBackground = true
     }
 
     func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
index 08891d8..b974a77 100644 (file)
@@ -96,7 +96,7 @@ extension InstructionsVC:SearchBarDelegate{
         else{
             var list =  [InstructionsModel]()
             for item in orginList {
-                if item.name.contains(text) {
+                if item.name.uppercased() .contains(text.uppercased()) {
                     list.append(item)
                 }
             }
index a6af4b4..d44307d 100644 (file)
@@ -22,7 +22,7 @@ enum AppSupportLanguage{
     case Singlish
     
     static var all:[AppSupportLanguage]{
-        return [.chinse,.Traditional_Chinese,.english,.japanese,.korean,.spanish,.vietnam,.thai,.malay,.Filipino,.indonesian,.french,.spanish,.russian,]
+        return [.chinse,.Traditional_Chinese,.english,.japanese,.korean,.spanish,.vietnam,.thai,.malay,.Filipino,.indonesian,.french,.Singlish,.russian]
     }
     var title:String{
         switch self {