Переглянути джерело

update player audio in editor, adblock webview

alqindiirsyam 5 місяців тому
батько
коміт
ab9e97f4a2

+ 7 - 7
AppBuilder/AppBuilder.xcodeproj/project.pbxproj

@@ -156,6 +156,7 @@
 			isa = PBXGroup;
 			children = (
 				CDE27BA42D53641D006298BD /* AppBuilder.entitlements */,
+				2401CEA7275490E600B323BB /* Info.plist */,
 				CD9D59D42BEE1D30008014B4 /* bi_icon.png */,
 				CD9D59D32BEE1D30008014B4 /* bpkh_icon.png */,
 				CD9D59D52BEE1D30008014B4 /* diginets_icon.png */,
@@ -168,17 +169,16 @@
 				CD9D59D12BEE1D2F008014B4 /* nxcook_icon.png */,
 				CD9D59D22BEE1D2F008014B4 /* nxsport_icon.png */,
 				2401CE99275490DB00B323BB /* AppDelegate.swift */,
+				A42ED92127F30BA200B0FAB7 /* FirstTabViewController.swift */,
+				12960ADF2892361000A467DD /* FourthTabViewController.swift */,
+				A413B18627EACB20006D16EB /* PrefsUtil.swift */,
 				2401CE9B275490DB00B323BB /* SceneDelegate.swift */,
+				A42ED92327F3FC2F00B0FAB7 /* SecondTabViewController.swift */,
+				A42ED92527F439A200B0FAB7 /* ThirdTabViewController.swift */,
 				2401CE9D275490DB00B323BB /* ViewController.swift */,
-				2401CE9F275490DB00B323BB /* Main.storyboard */,
 				2401CEA2275490E600B323BB /* Assets.xcassets */,
 				2401CEA4275490E600B323BB /* LaunchScreen.storyboard */,
-				2401CEA7275490E600B323BB /* Info.plist */,
-				A413B18627EACB20006D16EB /* PrefsUtil.swift */,
-				A42ED92127F30BA200B0FAB7 /* FirstTabViewController.swift */,
-				A42ED92327F3FC2F00B0FAB7 /* SecondTabViewController.swift */,
-				A42ED92527F439A200B0FAB7 /* ThirdTabViewController.swift */,
-				12960ADF2892361000A467DD /* FourthTabViewController.swift */,
+				2401CE9F275490DB00B323BB /* Main.storyboard */,
 			);
 			path = AppBuilder;
 			sourceTree = "<group>";

+ 37 - 7
AppBuilder/AppBuilder/FirstTabViewController.swift

@@ -27,7 +27,7 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
     var alertController = LibAlertController()
     
     public static var forceRefresh = true
-    public static var atFirstPage = true
+    public static var canLoadURL = false
     public static var showModal = false
     
     var indexImageVideoWv = 0
@@ -40,12 +40,20 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         super.viewDidLoad()
         
         self.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
-        
+
+        let configuration = WKWebViewConfiguration()
+        configuration.allowsInlineMediaPlayback = true
+        loadContentBlocker(into: configuration) { [self] in
+            DispatchQueue.main.async {
+                self.initializeWebView(with: configuration)
+            }
+        }
+    }
+    
+    func initializeWebView(with configuration: WKWebViewConfiguration) {
         let tapGesture = UITapGestureRecognizer(target: self, action: #selector(collapseDocked))
         tapGesture.cancelsTouchesInView = false
         tapGesture.delegate = self
-        let configuration = WKWebViewConfiguration()
-        configuration.allowsInlineMediaPlayback = true
         let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
         let finalUserAgent = "\(customUserAgent)"
         configuration.applicationNameForUserAgent = finalUserAgent
@@ -97,6 +105,22 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         
         NotificationCenter.default.addObserver(self, selector: #selector(onShowAC(notification:)), name: NSNotification.Name(rawValue: "onShowAC"), object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(onRefreshWebView(notification:)), name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil)
+        FirstTabViewController.canLoadURL = true
+        processURL()
+    }
+    
+    func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
+        // Define ad-blocking rules directly in Swift as a string
+        let contentRules = PrefsUtil.contentRulesAds
+
+        WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "AdBlocker", encodedContentRuleList: contentRules) { ruleList, error in
+            if let ruleList = ruleList {
+                config.userContentController.add(ruleList)
+            } else {
+                print("Failed to compile content rule list: \(error?.localizedDescription ?? "Unknown error")")
+            }
+            completion()
+        }
     }
     
     func loadURLWithCookie(url: URL) {
@@ -111,7 +135,7 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         }
     }
     
-    override func viewWillAppear(_ animated: Bool) {
+    func processURL() {
         let me = User.getMyPin()
         
         var myURL : URL?
@@ -156,14 +180,20 @@ class FirstTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
             }
         }
         if let u = myURL {
-            self.webView.evaluateJavaScript("{window.localStorage.setItem('currentTab','\(ViewController.sURL)')}")
+            webView.evaluateJavaScript("{window.localStorage.setItem('currentTab','\(ViewController.sURL)')}")
             if FirstTabViewController.forceRefresh {
                 loadURLWithCookie(url: u)
             } else {
-                self.webView.evaluateJavaScript("if(resumeAll){resumeAll();}")
+                webView.evaluateJavaScript("if(resumeAll){resumeAll();}")
             }
             FirstTabViewController.forceRefresh = false
         }
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        if FirstTabViewController.canLoadURL {
+            processURL()
+        }
         navigationController?.setNavigationBarHidden(true, animated: false)
     }
     

+ 117 - 116
AppBuilder/AppBuilder/PrefsUtil.swift

@@ -128,120 +128,121 @@ class PrefsUtil {
         return value
     }
     
+    static let contentRulesAds = """
+    [
+        {
+            "trigger": {
+                "url-filter": ".*ads.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*doubleclick.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*popads.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*popcash.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*onclickads.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*adfly.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*shorte.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*taboola.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*outbrain.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*scorecardresearch.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*google-analytics.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*facebook.com/tr.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*pixel.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        },
+        {
+            "trigger": {
+                "url-filter": ".*analytics.*"
+            },
+            "action": {
+                "type": "block"
+            }
+        }
+    ]
+    """
+    
 }
-//public static String getHeaderColorSetting(Context pContext) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        String header_color = sharedPref.getString("header_color", "#00000000");
-//        return header_color;
-//    }
-//
-//    public static String getHeaderTextColorSetting(Context pContext) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        String header_text_color = sharedPref.getString("header_text_color", "#FF000000");
-//        return header_text_color;
-//    }
-//
-//    public static void setContentFilter(Context pContext, String new_value) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        SharedPreferences.Editor edit = sharedPref.edit();
-//        edit.putString("content_filter",new_value);
-//        edit.apply();
-//    }
-//
-//    public static String getContentFilter(Context pContext) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        String header_text_color = sharedPref.getString("content_filter", "1,2,3,4,5");
-//        return header_text_color;
-//    }
-//
-//    public static void setContentSort(Context pContext, int new_value) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        SharedPreferences.Editor edit = sharedPref.edit();
-//        edit.putInt("content_sort",new_value);
-//        edit.apply();
-//    }
-//
-//    public static int getContentSort(Context pContext) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        int header_text_color = sharedPref.getInt("content_sort", 1);
-//        return header_text_color;
-//    }
-//
-//    public static void setContentClassification(Context pContext, String new_value) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        SharedPreferences.Editor edit = sharedPref.edit();
-//        edit.putString("content_class",new_value);
-//        edit.apply();
-//    }
-//
-//    public static String getContentClassification(Context pContext) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        String header_text_color = sharedPref.getString("content_class", "1,2,3,4");
-//        return header_text_color;
-//    }
-//
-//    public static final int CPAAS_MODE_FLOATING = 0;
-//    public static final int CPAAS_MODE_DOCKED = 1;
-//    public static final int CPAAS_MODE_BURGER = 2;
-//    public static final int CPAAS_MODE_MIX = 4;
-//    public static final int DEFAULT_CPAAS_MODE = CPAAS_MODE_DOCKED;
-//    public static int getCpaasMode(Context pContext) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        int mode_index = sharedPref.getInt("cpaas_mode", DEFAULT_CPAAS_MODE);
-//        return mode_index;
-//    }
-//
-//    public static void setCpaasMode(Context pContext, int new_value) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        SharedPreferences.Editor edit = sharedPref.edit();
-//        edit.putInt("cpaas_mode",new_value);
-//        edit.apply();
-//    }
-//
-//    public static final int CPAAS_FLOAT_MODE_IN_APP = 3;
-//    public static final int CPAAS_FLOAT_MODE_OUT_APP = 4;
-//    public static int getCpaasFloatMode(Context pContext) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        int mode_index = sharedPref.getInt("cpaas_float_mode", CPAAS_FLOAT_MODE_IN_APP);
-//        return mode_index;
-//    }
-//    public static void setCpaasFloatMode(Context pContext, int new_value) {
-//        final SharedPreferences sharedPref = pContext.getSharedPreferences("PREF_SYS", Context.MODE_PRIVATE);
-//        SharedPreferences.Editor edit = sharedPref.edit();
-//        edit.putInt("cpaas_float_mode",new_value);
-//        edit.apply();
-//    }
-//
-//    public static String getURLFirstTab() {
-//        return CoreDataSqlite_PrefsDB.get("app_builder_url_first_tab", "");
-//    }
-//
-//    public static String getURLThirdTab() {
-//        return  CoreDataSqlite_PrefsDB.get("app_builder_url_third_tab", "");
-//    }
-//
-//    public static String getURLBase() {
-//        return  CoreDataSqlite_PrefsDB.get("app_builder_url_base", Util_RandomCrypt.decrypt("3<rl;duhpt<<=vswwk"));
-//    }
-//
-//    public static void setURLFirstTab(String new_value) {
-//        CoreDataSqlite_PrefsDB.put("app_builder_url_first_tab", new_value);
-//    }
-//
-//    public static void setURLThirdTab(String new_value) {
-//        CoreDataSqlite_PrefsDB.put("app_builder_url_third_tab", new_value);
-//    }
-//
-//    public static void setURLBase(String new_value) {
-//        CoreDataSqlite_PrefsDB.put("app_builder_url_base", new_value);
-//    }
-//
-//    public static String DEFAULT_TAB_AMOUNT = "4";
-//
-//    public static String getTabAmount(){
-//        return CoreDataSqlite_PrefsDB.get("app_builder_tab_amount", DEFAULT_TAB_AMOUNT);
-//    }
-//
-//    public static void setTabAmount(Context pContext, String new_value){
-//        CoreDataSqlite_PrefsDB.put("app_builder_tab_amount", new_value);
-//    }

+ 35 - 17
AppBuilder/AppBuilder/ThirdTabViewController.swift

@@ -27,8 +27,7 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
     var alertController = LibAlertController()
     
     public static var forceRefresh = true
-    public static var inView = false
-    public static var atFirstPage = true
+    public static var canLoadURL = false
     public static var showModal = false
     
     var indexImageVideoWv = 0
@@ -41,12 +40,20 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         super.viewDidLoad()
         
         self.view.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
-        
+
+        let configuration = WKWebViewConfiguration()
+        configuration.allowsInlineMediaPlayback = true
+        loadContentBlocker(into: configuration) { [self] in
+            DispatchQueue.main.async {
+                self.initializeWebView(with: configuration)
+            }
+        }
+    }
+    
+    func initializeWebView(with configuration: WKWebViewConfiguration) {
         let tapGesture = UITapGestureRecognizer(target: self, action: #selector(collapseDocked))
         tapGesture.cancelsTouchesInView = false
         tapGesture.delegate = self
-        let configuration = WKWebViewConfiguration()
-        configuration.allowsInlineMediaPlayback = true
         let customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1 \(Utils.getUserAgent())"
         let finalUserAgent = "\(customUserAgent)"
         configuration.applicationNameForUserAgent = finalUserAgent
@@ -100,6 +107,22 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         NotificationCenter.default.addObserver(self, selector: #selector(onRefreshWebView(notification:)), name: NSNotification.Name(rawValue: "onRefreshWebView"), object: nil)
         
         imageVideoPicker = ImageVideoPicker(presentationController: self, delegate: self)
+        ThirdTabViewController.canLoadURL = true
+        processURL()
+    }
+    
+    func loadContentBlocker(into config: WKWebViewConfiguration, completion: @escaping () -> Void) {
+        // Define ad-blocking rules directly in Swift as a string
+        let contentRules = PrefsUtil.contentRulesAds
+
+        WKContentRuleListStore.default().compileContentRuleList(forIdentifier: "AdBlocker", encodedContentRuleList: contentRules) { ruleList, error in
+            if let ruleList = ruleList {
+                config.userContentController.add(ruleList)
+            } else {
+                print("Failed to compile content rule list: \(error?.localizedDescription ?? "Unknown error")")
+            }
+            completion()
+        }
     }
     
     func loadURLWithCookie(url: URL) {
@@ -114,11 +137,7 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         }
     }
     
-    override func viewWillAppear(_ animated: Bool) {
-        if ThirdTabViewController.inView {
-           return
-        }
-        ThirdTabViewController.inView = true
+    func processURL() {
         let me = User.getMyPin()
         
         var myURL : URL?
@@ -163,7 +182,6 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
                 }
             }
         }
-        //print(address)
         if let u = myURL{
             self.webView.evaluateJavaScript("{window.localStorage.setItem('currentTab','\(ViewController.tab3)')}")
             if ThirdTabViewController.forceRefresh {
@@ -173,6 +191,12 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
             }
             ThirdTabViewController.forceRefresh = false
         }
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        if ThirdTabViewController.canLoadURL {
+            processURL()
+        }
         navigationController?.setNavigationBarHidden(true, animated: false)
     }
     
@@ -184,11 +208,6 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
     
     override func viewDidAppear(_ animated: Bool) {
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
-//            if (self.isUsingMyWebview() && self.webView.url != nil && !self.webView.url!.absoluteString.contains("nexilis/pages/tab1-main-only") && !self.webView.url!.absoluteString.contains("nexilis/pages/tab3-main-only") && !self.webView.url!.absoluteString.contains("nexilis/pages/tab1-main") && !self.webView.url!.absoluteString.contains("nexilis/pages/tab3-commerce") && !self.webView.url!.absoluteString.contains("nexilis/pages/tab1-video") && !self.webView.url!.absoluteString.contains("nexilis/pages/tab3-main")) || ThirdTabViewController.showModal {
-//                ViewController.alwaysHideButton = true
-//                self.hideTabBar()
-//                ThirdTabViewController.atFirstPage = false
-//            } else {
                 var viewController = UIApplication.shared.windows.first!.rootViewController
                 if !(viewController is ViewController) {
                     viewController = self.parent
@@ -233,7 +252,6 @@ class ThirdTabViewController: UIViewController, UIScrollViewDelegate, UIGestureR
         self.webView.evaluateJavaScript("{if(pauseAll){pauseAll();}}")
         view.endEditing(true)
         resignFirstResponder()
-        ThirdTabViewController.inView = false
         self.webView.evaluateJavaScript("hideAddToCart();")
     }
     

+ 0 - 10
AppBuilder/AppBuilder/ViewController.swift

@@ -325,16 +325,6 @@ class ViewController: UITabBarController, UITabBarControllerDelegate, SettingMAB
         }
     }
     
-    func getPrefs(key: String) -> TMessage {
-        let tMessage = NexilisLite.TMessage()
-        let me = User.getMyPin()
-        tMessage.mCode = "PPR"
-        tMessage.mStatus = CoreMessage_TMessageUtil.getTID()
-        tMessage.mBodies[CoreMessage_TMessageKey.F_PIN] = me
-        tMessage.mBodies[CoreMessage_TMessageKey.KEY] = key
-        return tMessage
-    }
-    
     @objc func checkCounter() {
         DispatchQueue.global().async {
             DispatchQueue.main.async { [self] in

+ 49 - 2
AppBuilder/AppBuilderShare/ShareViewController.swift

@@ -209,13 +209,13 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
                             self.sendVideoToMainApp(dataVideotoCompress, dataShared)
                         }
                     }
-                } else if typeShareNum == TypeShare.file {
+                } else if typeShareNum == TypeShare.file || typeShareNum == TypeShare.audio {
                     let fileName = selectedFile.lastPathComponent
                     if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
                         let sharedFileURL = appGroupURL.appendingPathComponent(fileName)
                         try? Data(contentsOf: selectedFile).write(to: sharedFileURL)
                     }
-                    dataShared["file"] = fileName
+                    dataShared[typeShareNum == TypeShare.audio ? "audio" : "file"] = fileName
                     let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
                     if let jsonString = String(data: jsonData, encoding: .utf8) {
                         userDefaults.set(jsonString, forKey: "sharedItem")
@@ -593,6 +593,24 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
                     }
                 }
                 return
+            } else if attachment.hasItemConformingToTypeIdentifier(UTType.mpeg4Audio.identifier) || attachment.hasItemConformingToTypeIdentifier(UTType.mp3.identifier) || attachment.hasItemConformingToTypeIdentifier("org.opus-codec.opus") {
+                attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
+                    if let fileURL = fileItem as? URL {
+                        self.getExactAudioDuration(url: fileURL) { durationFormatted in
+                            let alert = UIAlertController(title: "Send to: \(contact.name)?", message: "File size: \(self.getExactFileSize(url: fileURL)) KB\nDuration: \(durationFormatted)", preferredStyle: .alert)
+                            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
+                            alert.addAction(UIAlertAction(title: "Send", style: .default, handler: {[self] _ in
+                                typeShareNum = TypeShare.audio
+                                selectedFile = fileURL
+                                sendAction()
+                            }))
+                            
+                            DispatchQueue.main.async {
+                                self.navigationController?.present(alert, animated: true, completion: nil)
+                            }
+                        }
+                    }
+                }
             } else if attachment.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
                 // Handle Other Files
                 attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
@@ -642,6 +660,34 @@ class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDat
             }
         }
     }
+
+    func getExactFileSize(url: URL) -> Int64 {
+        do {
+            let fileData = try Data(contentsOf: url)
+            let fileSizeInBytes = Int64(fileData.count)
+            return (fileSizeInBytes + 1023) / 1000  // Using 1000 instead of 1024
+        } catch {
+            print("Error reading file size: \(error)")
+        }
+        return 0
+    }
+    
+    func getExactAudioDuration(url: URL, completion: @escaping (String) -> Void) {
+        let asset = AVURLAsset(url: url)
+        asset.loadValuesAsynchronously(forKeys: ["duration"]) {
+            DispatchQueue.main.async {
+                let durationSeconds = CMTimeGetSeconds(asset.duration)
+
+                // Apply rounding logic like WhatsApp
+                let roundedDuration = Int(durationSeconds.rounded(.up))
+
+                let minutes = roundedDuration / 60
+                let seconds = roundedDuration % 60
+                let formattedDuration = String(format: "%d:%02d", minutes, seconds)
+                completion(formattedDuration)
+            }
+        }
+    }
 }
 
 struct Contact {
@@ -657,6 +703,7 @@ class TypeShare {
     static let image = 2
     static let video = 3
     static let file = 4
+    static let audio = 5
 }
 
 class VideoPreviewView: UIView {

+ 18 - 1
NexilisLite/NexilisLite/Source/APIS.swift

@@ -1274,6 +1274,7 @@ public class APIS: NSObject {
                                     let typeImage = 2
                                     let typeVideo = 3
                                     let typeFile = 4
+                                    let typeAudio = 5
                                     let typeContact = json["typeContact"] as? String ?? "0"
                                     var data = json["data"] as? String ?? ""
                                     let idContact = json["idContact"] as? String ?? ""
@@ -1286,7 +1287,9 @@ public class APIS: NSObject {
                                     let imageId = json["image"] as? String ?? ""
                                     let videoId = json["video"] as? String ?? ""
                                     let fileId = json["file"] as? String ?? ""
+                                    let audioId = json["audio"] as? String ?? ""
                                     var renamedFileId = ""
+                                    var renamedAudioId = ""
                                     var attachmentFlag = ""
                                     if scopeId == "4" {
                                         Database.shared.database?.inTransaction({ (fmdb, rollback) in
@@ -1351,8 +1354,22 @@ public class APIS: NSObject {
                                             data = "\(fileId)|\(data)"
                                         }
                                         attachmentFlag = "6"
+                                    } else if typeShare == typeAudio {
+                                        if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShared) {
+                                            renamedAudioId = "Nexilis_\(Date().currentTimeMillis())_" + audioId
+                                            let sharedFileURL = appGroupURL.appendingPathComponent(audioId)
+                                            let documentDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+                                            if FileManager.default.fileExists(atPath: sharedFileURL.path) {
+                                                let file = documentDir.appendingPathComponent(renamedAudioId)
+                                                if !FileManager().fileExists(atPath: file.path) {
+                                                    try? FileManager.default.copyItem(at: sharedFileURL, to: file)
+                                                }
+                                            }
+                                            data = "\(audioId)|\(data)"
+                                        }
+                                        attachmentFlag = "5"
                                     }
-                                    message = CoreMessage_TMessageBank.sendMessage(l_pin: groupId.isEmpty ? idContact : groupId, message_scope_id: scopeId, status: scopeId == "3" ? "1" : "2", message_text: data, credential: "0", attachment_flag: attachmentFlag, ex_blog_id: "", message_large_text: "", ex_format: "", image_id: imageId, audio_id: "", video_id: videoId, file_id: renamedFileId, thumb_id: thumb, reff_id: "", read_receipts: "4", chat_id: chatId, is_call_center: "0", call_center_id: "", opposite_pin: scopeId == "3" ? (User.getMyPin() ?? "") : idContact, gif_id: "", isForwarded: "0", isSecret: "0")
+                                    message = CoreMessage_TMessageBank.sendMessage(l_pin: groupId.isEmpty ? idContact : groupId, message_scope_id: scopeId, status: scopeId == "3" ? "1" : "2", message_text: data, credential: "0", attachment_flag: attachmentFlag, ex_blog_id: "", message_large_text: "", ex_format: "", image_id: imageId, audio_id: renamedAudioId, video_id: videoId, file_id: renamedFileId, thumb_id: thumb, reff_id: "", read_receipts: "4", chat_id: chatId, is_call_center: "0", call_center_id: "", opposite_pin: scopeId == "3" ? (User.getMyPin() ?? "") : idContact, gif_id: "", isForwarded: "0", isSecret: "0")
                                     Nexilis.addQueueMessage(message: message)
                                     userDefaults.set("", forKey: "sharedItem")
                                     userDefaults.synchronize()

+ 11 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageBank.swift

@@ -2655,4 +2655,15 @@ public class CoreMessage_TMessageBank {
         return tMessage
     }
     
+    public static func getPref(key: String) -> TMessage {
+        let tMessage = NexilisLite.TMessage()
+        let me = User.getMyPin() ?? ""
+        tMessage.mPIN = me
+        tMessage.mCode = CoreMessage_TMessageCode.GET_PUSH_PREFS
+        tMessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tMessage.mBodies[CoreMessage_TMessageKey.F_PIN] = me
+        tMessage.mBodies[CoreMessage_TMessageKey.KEY] = key
+        return tMessage
+    }
+    
 }

+ 2 - 0
NexilisLite/NexilisLite/Source/CoreMessage_TMessageCode.swift

@@ -801,4 +801,6 @@ public class CoreMessage_TMessageCode {
     public static let ACCEPT_REJECT_MEETING = "MTG";
     public static let GET_CHATBOT_SCHEDULE = "CHS";
     public static let GPT_SERVICE = "GPTS";
+    
+    public static let GET_PUSH_PREFS = "GPR";
 }

+ 1 - 1
NexilisLite/NexilisLite/Source/FileEncryption.swift

@@ -53,7 +53,7 @@ public class FileEncryption {
         do {
             try encryptedData?.write(to: outputURL)
             try fileManager.removeItem(at: inputURL)
-            print("File deleted successfully")
+//            print("File deleted successfully")
         } catch {
             print("Error deleting file: \(error)")
         }

+ 0 - 1
NexilisLite/NexilisLite/Source/Network.swift

@@ -218,7 +218,6 @@ public class Network {
                         let fileDirServer = tempDir.appendingPathComponent(filenameServer)
                         let fileURLServer = URL(fileURLWithPath: fileDirServer.path)
                         try FileEncryption.shared.encryptFile(fileURL, fileURLServer, MasterKeyUtil.shared.getServerKey())
-                        print("ADA KAN? \(fileURL) <><> \(fileURLServer)")
 //                        let dataSecure = try FileEncryption.shared.encryptFile(fileURL)
 //                        dataSecure?.write(to: fileURLSecure)
                         filesIn.append(fileURL)

+ 39 - 0
NexilisLite/NexilisLite/Source/Utils.swift

@@ -803,6 +803,15 @@ public final class Utils {
                     if Array(json.keys)[i] == "gptbot_url" {
                         Utils.setGPTBotUrl(value: Array(json.values)[i] as? String ?? "")
                     }
+                    if Array(json.keys)[i] == "default_sound_incmsg" {
+                        Utils.setDefaultIncomingMsg(value: Array(json.values)[i] as? String ?? "")
+                    }
+                    if Array(json.keys)[i] == "default_sound_inccall" {
+                        Utils.setDefaultIncomingCall(value: Array(json.values)[i] as? String ?? "")
+                    }
+                    if Array(json.keys)[i] == "default_sound_rbt" {
+                        Utils.setDefaultIncomingRBT(value: Array(json.values)[i] as? String ?? "")
+                    }
                 }
                 Utils.setFinishInitPrefs(value: true)
                 DispatchQueue.main.async {
@@ -1325,6 +1334,36 @@ public final class Utils {
         return 0
     }
     
+    public static func setDefaultIncomingMsg(value: String) {
+        SecureUserDefaults.shared.set(value, forKey: "default_sound_incmsg")
+    }
+    public static func getDefaultIncomingMsg() -> String {
+        if let value: String = SecureUserDefaults.shared.value(forKey: "default_sound_incmsg") {
+            return value
+        }
+        return ""
+    }
+    
+    public static func setDefaultIncomingCall(value: String) {
+        SecureUserDefaults.shared.set(value, forKey: "default_sound_inccall")
+    }
+    public static func getDefaultIncomingCall() -> String {
+        if let value: String = SecureUserDefaults.shared.value(forKey: "default_sound_inccall") {
+            return value
+        }
+        return ""
+    }
+    
+    public static func setDefaultIncomingRBT(value: String) {
+        SecureUserDefaults.shared.set(value, forKey: "default_sound_rbt")
+    }
+    public static func getDefaultIncomingRBT() -> String {
+        if let value: String = SecureUserDefaults.shared.value(forKey: "default_sound_rbt") {
+            return value
+        }
+        return ""
+    }
+    
     static func getPasswordDB() -> String? {
         do {
             let p = getPassEncDB()

+ 124 - 44
NexilisLite/NexilisLite/Source/View/Chat/EditorGroup.swift

@@ -101,7 +101,6 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     var lastY: CGFloat = 0
     var listTimerCredential: [String: Int] = [:]
     var timerCredential: [String: Timer] = [:]
-    var audioPlayer: AVAudioPlayer?
     var editVC = UIViewController()
     var editTextView = CustomTextView()
     var isEditingMessage = false
@@ -114,6 +113,10 @@ public class EditorGroup: UIViewController, CLLocationManagerDelegate {
     var isBlackCancelButton = false
     let buttonSendEdit = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
     
+    var audioPlayers: [IndexPath: AVAudioPlayer] = [:]
+    var timers: [IndexPath: Timer] = [:]
+    var playingIndexPath: IndexPath?
+    
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
             removeAllObjectBeforeDismissVC()
@@ -3984,7 +3987,7 @@ extension EditorGroup: UICollectionViewDelegate, UICollectionViewDataSource {
     }
 }
 
-extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
+extension EditorGroup: UITableViewDelegate, UITableViewDataSource, AVAudioPlayerDelegate {
     //    public func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
     //        checkNewMessage(tableView: tableView)
     //    }
@@ -4746,17 +4749,41 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
         let imageGif = SDAnimatedImageView()
         
         if !audioChat.isEmpty {
+            messageText.isHidden = true
             let imageAudio = UIImageView()
             imageAudio.image = UIImage(systemName: "music.note", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35))
             containerMessage.addSubview(imageAudio)
             imageAudio.anchor(left: containerMessage.leftAnchor, paddingLeft: 15, centerY: containerMessage.centerYAnchor)
-            imageAudio.tintColor = .black
+            imageAudio.tintColor = .mainColor
+            
+            let playButtonAudio = UIButton(type: .system)
+            playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
+            playButtonAudio.tintColor = .gray
+            containerMessage.addSubview(playButtonAudio)
+            playButtonAudio.anchor(left: containerMessage.leftAnchor, paddingLeft: 60, centerY: containerMessage.centerYAnchor, width: 20, height: 20)
+            
+            let progressSliderAudio = UISlider()
+            progressSliderAudio.minimumValue = 0
+            progressSliderAudio.maximumValue = 1
+            let thumbImage = UIImage(systemName: "circle.fill")?.withTintColor(UIColor.mainColor)
+                .resize(target: CGSize(width: 15, height: 15))
+            progressSliderAudio.setThumbImage(thumbImage, for: .normal)
+            containerMessage.addSubview(progressSliderAudio)
+            progressSliderAudio.anchor(top: containerMessage.topAnchor, left: playButtonAudio.rightAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: 15, paddingLeft: 10, paddingBottom: 15, paddingRight: 15)
+            
+            let timeLabelAudio = UILabel()
+            timeLabelAudio.text = "0:00"
+            timeLabelAudio.font = .systemFont(ofSize: 10)
+            timeLabelAudio.textColor = .gray
+            containerMessage.addSubview(timeLabelAudio)
+            timeLabelAudio.anchor(top: playButtonAudio.bottomAnchor, left: playButtonAudio.rightAnchor, paddingLeft: 10, width: 100, height: 12)
             
             let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
             let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
             let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
             if let dirPath = paths.first {
                 let audioURL = URL(fileURLWithPath: dirPath).appendingPathComponent(audioChat)
+                var url = audioURL
                 if !FileManager.default.fileExists(atPath: audioURL.path) && !FileEncryption.shared.isSecureExists(filename: audioChat) {
                     Download().startHTTP(forKey: audioChat, isImage: false) { (name, progress) in
                         guard progress == 100 else {
@@ -4764,11 +4791,47 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                         }
                         tableView.reloadRows(at: [indexPath], with: .none)
                     }
+                } else {
+                    if !FileManager.default.fileExists(atPath: audioURL.path) {
+                        do {
+                            if let audioData = try FileEncryption.shared.readSecure(filename: audioChat) {
+                                let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+                                let tempPath = cachesDirectory.appendingPathComponent(audioChat)
+                                try audioData.write(to: tempPath)
+                                url = tempPath
+                            }
+                        } catch {
+                            
+                        }
+                    }
+                }
+                if audioPlayers[indexPath] == nil {
+                    do {
+                        let audioPlayer = try AVAudioPlayer(contentsOf: url)
+                        audioPlayers[indexPath] = audioPlayer
+                        audioPlayer.delegate = self
+                        progressSliderAudio.maximumValue = Float(audioPlayer.duration)
+                        timeLabelAudio.text = formatTime(audioPlayer.duration)
+                    } catch {
+                        print("Error loading audio: \(error)")
+                    }
+                }
+                let audioPlayer = audioPlayers[indexPath]
+                if playingIndexPath == indexPath, let player = audioPlayer, player.isPlaying {
+                    playButtonAudio.setImage(UIImage(systemName: "pause.fill"), for: .normal)
+                } else {
+                    playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
                 }
+
+                // Play/Pause Button Action
+                playButtonAudio.addAction(UIAction { _ in
+                    self.playPauseAudio(indexPath: indexPath, playButton: playButtonAudio, progressSlider: progressSliderAudio, timeLabel: timeLabelAudio)
+                }, for: .touchUpInside)
+                
+                progressSliderAudio.addAction(UIAction { _ in
+                    self.sliderChanged(indexPath: indexPath, progressSlider: progressSliderAudio, timeLabel: timeLabelAudio)
+                }, for: .valueChanged)
             }
-            let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
-            containerMessage.addGestureRecognizer(objectTap)
-            objectTap.audio_id = audioChat
         }
         
         if (thumbChat != "" && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"]  as? String ?? "" != "1")) {
@@ -5562,6 +5625,61 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
         return cellMessage
     }
     
+    func playPauseAudio(indexPath: IndexPath, playButton: UIButton, progressSlider: UISlider, timeLabel: UILabel) {
+        guard let audioPlayer = audioPlayers[indexPath] else { return }
+
+        if audioPlayer.isPlaying {
+            // Pause Audio
+            audioPlayer.pause()
+            playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
+            timers[indexPath]?.invalidate()
+        } else {
+            // Stop other players if one is already playing
+            if let currentPlayingIndexPath = playingIndexPath, let currentAudioPlayer = audioPlayers[currentPlayingIndexPath] {
+                currentAudioPlayer.pause()
+                timers[currentPlayingIndexPath]?.invalidate()
+                timers[currentPlayingIndexPath] = nil
+                tableChatView.reloadRows(at: [currentPlayingIndexPath], with: .none)
+            }
+
+            // Play new audio
+            audioPlayer.play()
+            playButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
+            playingIndexPath = indexPath
+
+            // Start timer to update progress
+            timers[indexPath] = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
+                progressSlider.value = Float(audioPlayer.currentTime)
+                timeLabel.text = self.formatTime(audioPlayer.currentTime)
+            }
+        }
+    }
+    
+    func sliderChanged(indexPath: IndexPath, progressSlider: UISlider, timeLabel: UILabel) {
+        guard let audioPlayer = audioPlayers[indexPath] else { return }
+        audioPlayer.currentTime = TimeInterval(progressSlider.value)
+        timeLabel.text = formatTime(audioPlayer.currentTime)
+    }
+    
+    func formatTime(_ time: TimeInterval) -> String {
+        let roundedTime = time.rounded(.up)
+        let minutes = Int(roundedTime) / 60
+        let seconds = Int(roundedTime) % 60
+        return String(format: "%d:%02d", minutes, seconds)
+    }
+    
+    public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        if let finishedIndexPath = audioPlayers.first(where: { $0.value == player })?.key {
+           DispatchQueue.main.async {
+               self.timers[finishedIndexPath]?.invalidate()
+               self.timers[finishedIndexPath] = nil
+               self.playingIndexPath = nil
+               self.audioPlayers[finishedIndexPath] = nil
+               self.tableChatView.reloadRows(at: [finishedIndexPath], with: .none)
+           }
+        }
+    }
+    
     @objc func imageGroupingTapped(_ sender: ObjectGesture) {
         let listGroupingImages = ListGroupImages()
         listGroupingImages.imageTapped = sender.indexImageTapped
@@ -5988,44 +6106,6 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                     }
                 }
             }
-        } else if !sender.audio_id.isEmpty {
-            if let dirPath = paths.first {
-                let audioURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.audio_id)
-                if FileManager.default.fileExists(atPath: audioURL.path) {
-                    do {
-                        if audioPlayer == nil || audioPlayer?.url != audioURL {
-                            audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
-                            audioPlayer?.prepareToPlay()
-                            audioPlayer?.play()
-                        } else if audioPlayer!.isPlaying {
-                            audioPlayer?.pause()
-                        } else {
-                            audioPlayer?.play()
-                        }
-                    } catch {
-                        
-                    }
-                } else if FileEncryption.shared.isSecureExists(filename: sender.audio_id) {
-                    do {
-                        if let audioData = try FileEncryption.shared.readSecure(filename: sender.audio_id) {
-                            let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-                            let tempPath = cachesDirectory.appendingPathComponent(sender.audio_id)
-                            try audioData.write(to: tempPath)
-                            if audioPlayer == nil || audioPlayer?.url != tempPath {
-                                audioPlayer = try AVAudioPlayer(contentsOf: tempPath)
-                                audioPlayer?.prepareToPlay()
-                                audioPlayer?.play()
-                            } else if audioPlayer!.isPlaying {
-                                audioPlayer?.pause()
-                            } else {
-                                audioPlayer?.play()
-                            }
-                        }
-                    } catch {
-                        
-                    }
-                }
-            }
         } else {
             DispatchQueue.main.async {
                 let idx = self.dataMessages.firstIndex(where: { $0["message_id"]  as? String ?? "" == sender.message_id})

+ 124 - 45
NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift

@@ -112,7 +112,6 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var groupImages: [String:[ImageGrouping]] = [:]
     var titleText: String!
     var lastY: CGFloat = 0
-    var audioPlayer: AVAudioPlayer?
     var editVC = UIViewController()
     var editTextView = CustomTextView()
     var isEditingMessage = false
@@ -125,6 +124,10 @@ public class EditorPersonal: UIViewController, ImageVideoPickerDelegate, UIGestu
     var isBlackCancelButton = false
     let buttonSendEdit = UIButton(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
     
+    var audioPlayers: [IndexPath: AVAudioPlayer] = [:]
+    var timers: [IndexPath: Timer] = [:]
+    var playingIndexPath: IndexPath?
+    
     public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
             removeAllObjectBeforeDismissVC()
@@ -5074,7 +5077,7 @@ extension EditorPersonal: UICollectionViewDelegate, UICollectionViewDataSource {
 }
 
 //ETB
-extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
+extension EditorPersonal: UITableViewDelegate, UITableViewDataSource, AVAudioPlayerDelegate {
 //    public func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
 //        checkNewMessage(tableView: tableView)
 //    }
@@ -6001,17 +6004,41 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         let imageGif = SDAnimatedImageView()
         
         if !audioChat.isEmpty {
+            messageText.isHidden = true
             let imageAudio = UIImageView()
             imageAudio.image = UIImage(systemName: "music.note", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35))
             containerMessage.addSubview(imageAudio)
             imageAudio.anchor(left: containerMessage.leftAnchor, paddingLeft: 15, centerY: containerMessage.centerYAnchor)
-            imageAudio.tintColor = .black
+            imageAudio.tintColor = .mainColor
+            
+            let playButtonAudio = UIButton(type: .system)
+            playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
+            playButtonAudio.tintColor = .gray
+            containerMessage.addSubview(playButtonAudio)
+            playButtonAudio.anchor(left: containerMessage.leftAnchor, paddingLeft: 60, centerY: containerMessage.centerYAnchor, width: 20, height: 20)
+            
+            let progressSliderAudio = UISlider()
+            progressSliderAudio.minimumValue = 0
+            progressSliderAudio.maximumValue = 1
+            let thumbImage = UIImage(systemName: "circle.fill")?.withTintColor(UIColor.mainColor)
+                .resize(target: CGSize(width: 15, height: 15))
+            progressSliderAudio.setThumbImage(thumbImage, for: .normal)
+            containerMessage.addSubview(progressSliderAudio)
+            progressSliderAudio.anchor(top: containerMessage.topAnchor, left: playButtonAudio.rightAnchor, bottom: containerMessage.bottomAnchor, right: containerMessage.rightAnchor, paddingTop: 15, paddingLeft: 10, paddingBottom: 15, paddingRight: 15)
+            
+            let timeLabelAudio = UILabel()
+            timeLabelAudio.text = "0:00"
+            timeLabelAudio.font = .systemFont(ofSize: 10)
+            timeLabelAudio.textColor = .gray
+            containerMessage.addSubview(timeLabelAudio)
+            timeLabelAudio.anchor(top: playButtonAudio.bottomAnchor, left: playButtonAudio.rightAnchor, paddingLeft: 10, width: 100, height: 12)
             
             let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
             let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
             let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
             if let dirPath = paths.first {
                 let audioURL = URL(fileURLWithPath: dirPath).appendingPathComponent(audioChat)
+                var url = audioURL
                 if !FileManager.default.fileExists(atPath: audioURL.path) && !FileEncryption.shared.isSecureExists(filename: audioChat) {
                     Download().startHTTP(forKey: audioChat, isImage: false) { (name, progress) in
                         guard progress == 100 else {
@@ -6019,11 +6046,47 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
                         }
                         tableView.reloadRows(at: [indexPath], with: .none)
                     }
+                } else {
+                    if !FileManager.default.fileExists(atPath: audioURL.path) {
+                        do {
+                            if let audioData = try FileEncryption.shared.readSecure(filename: audioChat) {
+                                let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
+                                let tempPath = cachesDirectory.appendingPathComponent(audioChat)
+                                try audioData.write(to: tempPath)
+                                url = tempPath
+                            }
+                        } catch {
+                            
+                        }
+                    }
+                }
+                if audioPlayers[indexPath] == nil {
+                    do {
+                        let audioPlayer = try AVAudioPlayer(contentsOf: url)
+                        audioPlayers[indexPath] = audioPlayer
+                        audioPlayer.delegate = self
+                        progressSliderAudio.maximumValue = Float(audioPlayer.duration)
+                        timeLabelAudio.text = formatTime(audioPlayer.duration)
+                    } catch {
+                        print("Error loading audio: \(error)")
+                    }
+                }
+                let audioPlayer = audioPlayers[indexPath]
+                if playingIndexPath == indexPath, let player = audioPlayer, player.isPlaying {
+                    playButtonAudio.setImage(UIImage(systemName: "pause.fill"), for: .normal)
+                } else {
+                    playButtonAudio.setImage(UIImage(systemName: "play.fill"), for: .normal)
                 }
+
+                // Play/Pause Button Action
+                playButtonAudio.addAction(UIAction { _ in
+                    self.playPauseAudio(indexPath: indexPath, playButton: playButtonAudio, progressSlider: progressSliderAudio, timeLabel: timeLabelAudio)
+                }, for: .touchUpInside)
+                
+                progressSliderAudio.addAction(UIAction { _ in
+                    self.sliderChanged(indexPath: indexPath, progressSlider: progressSliderAudio, timeLabel: timeLabelAudio)
+                }, for: .valueChanged)
             }
-            let objectTap = ObjectGesture(target: self, action: #selector(contentMessageTapped(_:)))
-            containerMessage.addGestureRecognizer(objectTap)
-            objectTap.audio_id = audioChat
         }
         
         if (!thumbChat.isEmpty && (dataMessages[indexPath.row]["lock"] == nil || dataMessages[indexPath.row]["lock"]  as? String ?? "" != "1") && (dataMessages[indexPath.row]["lock"] as? String != "2")) {
@@ -6805,6 +6868,61 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         return cell
     }
     
+    func playPauseAudio(indexPath: IndexPath, playButton: UIButton, progressSlider: UISlider, timeLabel: UILabel) {
+        guard let audioPlayer = audioPlayers[indexPath] else { return }
+
+        if audioPlayer.isPlaying {
+            // Pause Audio
+            audioPlayer.pause()
+            playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
+            timers[indexPath]?.invalidate()
+        } else {
+            // Stop other players if one is already playing
+            if let currentPlayingIndexPath = playingIndexPath, let currentAudioPlayer = audioPlayers[currentPlayingIndexPath] {
+                currentAudioPlayer.pause()
+                timers[currentPlayingIndexPath]?.invalidate()
+                timers[currentPlayingIndexPath] = nil
+                tableChatView.reloadRows(at: [currentPlayingIndexPath], with: .none)
+            }
+
+            // Play new audio
+            audioPlayer.play()
+            playButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
+            playingIndexPath = indexPath
+
+            // Start timer to update progress
+            timers[indexPath] = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
+                progressSlider.value = Float(audioPlayer.currentTime)
+                timeLabel.text = self.formatTime(audioPlayer.currentTime)
+            }
+        }
+    }
+    
+    func sliderChanged(indexPath: IndexPath, progressSlider: UISlider, timeLabel: UILabel) {
+        guard let audioPlayer = audioPlayers[indexPath] else { return }
+        audioPlayer.currentTime = TimeInterval(progressSlider.value)
+        timeLabel.text = formatTime(audioPlayer.currentTime)
+    }
+    
+    func formatTime(_ time: TimeInterval) -> String {
+        let roundedTime = time.rounded(.up)
+        let minutes = Int(roundedTime) / 60
+        let seconds = Int(roundedTime) % 60
+        return String(format: "%d:%02d", minutes, seconds)
+    }
+    
+    public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        if let finishedIndexPath = audioPlayers.first(where: { $0.value == player })?.key {
+           DispatchQueue.main.async {
+               self.timers[finishedIndexPath]?.invalidate()
+               self.timers[finishedIndexPath] = nil
+               self.playingIndexPath = nil
+               self.audioPlayers[finishedIndexPath] = nil
+               self.tableChatView.reloadRows(at: [finishedIndexPath], with: .none)
+           }
+        }
+    }
+    
     @objc func imageGroupingTapped(_ sender: ObjectGesture) {
         let listGroupingImages = ListGroupImages()
         listGroupingImages.imageTapped = sender.indexImageTapped
@@ -7209,7 +7327,6 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
         } else if (sender.file_id != "") {
             if let dirPath = paths.first {
                 let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.file_id)
-                print("MASUK SINI KAH? \(fileURL)")
                 if FileManager.default.fileExists(atPath: fileURL.path) {
                     self.previewItem = fileURL as NSURL
                     let previewController = QLPreviewController()
@@ -7293,44 +7410,6 @@ extension EditorPersonal: UITableViewDelegate, UITableViewDataSource {
                     }
                 }
             }
-        } else if !sender.audio_id.isEmpty {
-            if let dirPath = paths.first {
-                let audioURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.audio_id)
-                if FileManager.default.fileExists(atPath: audioURL.path) {
-                    do {
-                        if audioPlayer == nil || audioPlayer?.url != audioURL {
-                            audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
-                            audioPlayer?.prepareToPlay()
-                            audioPlayer?.play()
-                        } else if audioPlayer!.isPlaying {
-                            audioPlayer?.pause()
-                        } else {
-                            audioPlayer?.play()
-                        }
-                    } catch {
-                        
-                    }
-                } else if FileEncryption.shared.isSecureExists(filename: sender.audio_id) {
-                    do {
-                        if let audioData = try FileEncryption.shared.readSecure(filename: sender.audio_id) {
-                            let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
-                            let tempPath = cachesDirectory.appendingPathComponent(sender.audio_id)
-                            try audioData.write(to: tempPath)
-                            if audioPlayer == nil || audioPlayer?.url != tempPath {
-                                audioPlayer = try AVAudioPlayer(contentsOf: tempPath)
-                                audioPlayer?.prepareToPlay()
-                                audioPlayer?.play()
-                            } else if audioPlayer!.isPlaying {
-                                audioPlayer?.pause()
-                            } else {
-                                audioPlayer?.play()
-                            }
-                        }
-                    } catch {
-                        
-                    }
-                }
-            }
         } else {
             DispatchQueue.main.async {
                 let idx = self.dataMessages.firstIndex(where: { $0["message_id"] as? String == sender.message_id})

+ 6 - 7
NexilisLite/NexilisLite/Source/View/Control/NotificationSound.swift

@@ -15,7 +15,6 @@ public class NotificationSound: UIViewController, UITableViewDelegate, UITableVi
     var data: [NotifSound] = []
     var isSelectedSound = 0
     var lastSelectedSound = 0
-    var audioPlayer: AVAudioPlayer?
 
     public override func viewDidLoad() {
         super.viewDidLoad()
@@ -92,8 +91,8 @@ public class NotificationSound: UIViewController, UITableViewDelegate, UITableVi
             SecureUserDefaults.shared.set("\(data[idx!].id):\(data[idx!].name)", forKey: "newNotifSoundGroup")
         }
         //stopSound
-        if audioPlayer != nil && audioPlayer!.isPlaying {
-            audioPlayer?.stop()
+        if Nexilis.sharedAudioPlayer != nil && Nexilis.sharedAudioPlayer!.isPlaying {
+            Nexilis.sharedAudioPlayer?.stop()
         }
         navigationController?.dismiss(animated: true, completion: nil)
     }
@@ -108,7 +107,7 @@ public class NotificationSound: UIViewController, UITableViewDelegate, UITableVi
         data[idxNew!].isSelected = true
         if lastSelectedSound != 0 {
             //stopSound
-            audioPlayer?.stop()
+            Nexilis.sharedAudioPlayer?.stop()
         }
         lastSelectedSound = data[indexPath.row].id
         isSelectedSound = data[indexPath.row].id
@@ -122,9 +121,9 @@ public class NotificationSound: UIViewController, UITableViewDelegate, UITableVi
             soundURL = Bundle.resourcesMediaBundle(for: Nexilis.self).url(forResource: nameSound, withExtension: "mp3")
         }
         do {
-            audioPlayer = try AVAudioPlayer(contentsOf: soundURL!)
-            audioPlayer?.prepareToPlay()
-            audioPlayer?.play()
+            Nexilis.sharedAudioPlayer = try AVAudioPlayer(contentsOf: soundURL!)
+            Nexilis.sharedAudioPlayer?.prepareToPlay()
+            Nexilis.sharedAudioPlayer?.play()
         } catch {
             
         }