Răsfoiți Sursa

feat: 支持 Universal links

陈文艺 3 luni în urmă
părinte
comite
225b53cb3e

+ 5 - 3
Lanu.xcodeproj/project.pbxproj

@@ -249,8 +249,8 @@
 				Views/Profile/Mine/LNMineWalletInfoView.swift,
 				Views/Profile/Post/LNPostShareImageGenerator.swift,
 				Views/Profile/Post/LNPostShareSkillItemView.swift,
+				Views/Profile/Post/LNPostShareViewController.swift,
 				Views/Profile/Post/LNPostSkillSelectPanel.swift,
-				Views/Profile/Post/LNSharePostViewController.swift,
 				Views/Profile/Profile/LNProfileBottomMenu.swift,
 				Views/Profile/Profile/LNProfileInfosView.swift,
 				Views/Profile/Profile/LNProfileNaviBarView.swift,
@@ -528,8 +528,9 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Lanu/Lanu.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 13;
+				CURRENT_PROJECT_VERSION = 14;
 				DEVELOPMENT_TEAM = 5H8D98R72W;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -571,8 +572,9 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = Lanu/Lanu.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 13;
+				CURRENT_PROJECT_VERSION = 14;
 				DEVELOPMENT_TEAM = 5H8D98R72W;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;

+ 11 - 0
Lanu/Lanu.entitlements

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.developer.associated-domains</key>
+	<array>
+		<string>applinks:test-web.gami.vip</string>
+		<string>applinks:web.gami.vip</string>
+	</array>
+</dict>
+</plist>

+ 29 - 6
Lanu/Manager/Deeplink/LNDeeplinkManager.swift

@@ -23,6 +23,11 @@ struct LNDeeplinkUrls {
             case order
         }
     }
+    
+    enum user: String {
+        case category
+        case profile
+    }
 }
 
 extension RawRepresentable where RawValue == String {
@@ -65,14 +70,18 @@ class LNDeeplinkManager {
         DispatchQueue.global().async { [weak self] in
             guard let self else { return }
             
-            guard url.scheme?.lowercased() == LNDeeplinkUrls.appScheme else {
-                return
-            }
-            let fullPath = if let host = url.host {
-                "\(host)\(url.path)"
+            var fullPath = if url.scheme?.lowercased() == LNDeeplinkUrls.appScheme {
+                if let host = url.host {
+                    "\(host)\(url.path)"
+                } else {
+                    url.path
+                }
             } else {
                 url.path
             }
+            if fullPath.hasPrefix("/") {
+                fullPath = String(fullPath.dropFirst())
+            }
             
             let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
             let params: [String: Any] = components?.queryItems?.reduce(into: [String: Any]())
@@ -121,12 +130,26 @@ extension LNDeeplinkManager {
             guard let param, !param.uid.isEmpty else { return }
             UIView.appKeyWindow?.pushToProfile(uid: param.uid)
         }
+        register(url: LNDeeplinkUrls.user.profile.deeplinkPath) { (param: LNProfileDeeplinkParams?) in
+            guard let param, !param.id.isEmpty else { return }
+            UIView.appKeyWindow?.pushToProfile(uid: param.id)
+        }
     }
     
     private func setupOrderDeeplink() {
         register(url: LNDeeplinkUrls.app.qrcode.order.deeplinkPath) { (param: LNORCodeCreateOrderParams?) in
             guard let param, !param.qrcode.isEmpty else { return }
-            UIView.appKeyWindow?.pushToCreateOrder(param.qrcode)
+            LNOrderManager.shared.getQRDetail(data: param.qrcode) { res in
+                guard let res else { return }
+                UIView.appKeyWindow?.pushToCreateOrder(res)
+            }
+        }
+        register(url: LNDeeplinkUrls.user.category.deeplinkPath) { (param: LNUserSkillParams?) in
+            guard let param, !param.code.isEmpty else { return }
+            LNOrderManager.shared.getQRDetail(data: param.code) { res in
+                guard let res else { return }
+                UIView.appKeyWindow?.pushToCreateOrder(res)
+            }
         }
     }
 }

+ 7 - 0
Lanu/Manager/Deeplink/LNDeeplinkParams.swift

@@ -19,9 +19,16 @@ class LNWebPageDeeplinkParams: Decodable {
 @AutoCodable
 class LNProfileDeeplinkParams: Decodable {
     var uid: String = ""
+    var id: String = ""
 }
 
 @AutoCodable
 class LNORCodeCreateOrderParams: Decodable {
     var qrcode: String = ""
 }
+
+@AutoCodable
+class LNUserSkillParams: Decodable {
+    var code: String = ""
+    var id: String = ""
+}

+ 7 - 0
Lanu/Manager/Network/LNHttpManager.swift

@@ -42,6 +42,10 @@ enum LNHttpError: Error, LocalizedError {
     }
 }
 
+enum LNHttpCommonErrorCode: Int {
+    case beKicked = 100004
+}
+
 /// HTTP请求管理器
 class LNHttpManager {
     static let shared = LNHttpManager()
@@ -227,6 +231,9 @@ class LNHttpManager {
             decoder.keyDecodingStrategy = .convertFromSnakeCase // 处理蛇形命名
             let result = try decoder.decode(LNHttpResponse<T>.self, from: data)
             if result.code != 0 {
+                if result.code == LNHttpCommonErrorCode.beKicked.rawValue {
+                    LNAccountManager.shared.clean()
+                }
                 completion(nil, .serverError(result.code, result.msg))
             } else {
                 completion(result.data, nil)

+ 8 - 0
Lanu/Manager/Order/LNOrderManager.swift

@@ -68,6 +68,14 @@ extension LNOrderManager {
             }
         }
     }
+    
+    func getLastOrder(queue: DispatchQueue = .main, handler: @escaping (LNLastOrderResponse?) -> Void) {
+        LNHttpManager.shared.getLastOrder { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res)
+            }
+        }
+    }
 }
 
 

+ 5 - 0
Lanu/Manager/Order/Network/LNHttpManager+Order.swift

@@ -13,6 +13,7 @@ private let kNetPath_Order_Create = "/skill/order/payment"
 private let kNetPath_Order_List = "/skill/order/list"
 private let kNetPath_Order_Detail = "/skill/order/detail"
 private let kNetPath_Order_Unfinished = "/skill/order/unfinishedWith"
+private let kNetPath_Order_Last = "/playmate/mylast/orderinfo"
 
 private let kNetPath_Order_Finish = "/skill/order/finish"
 private let kNetPath_Order_Refund = "/skill/order/refund/apply"
@@ -86,6 +87,10 @@ extension LNHttpManager {
             ]
         ], completion: completion)
     }
+    
+    func getLastOrder(completion: @escaping (LNLastOrderResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Order_Last, completion: completion)
+    }
 }
 
 extension LNHttpManager {

+ 8 - 0
Lanu/Manager/Order/Network/LNOrderResponse.swift

@@ -161,3 +161,11 @@ class LNQRCodeDetailResponse: Decodable {
     var currencyAmount: Double = 0
     var sellerUserNo: String = ""
 }
+
+@AutoCodable
+class LNLastOrderResponse: Decodable {
+    var avatar: String = ""
+    var nickname: String = ""
+    var finishTime: Int = 0
+    var bizCateGoryName: String = ""
+}

+ 18 - 0
Lanu/SceneDelegate.swift

@@ -15,6 +15,10 @@ import DoraemonKit
 class SceneDelegate: UIResponder, UIWindowSceneDelegate {
     var window: UIWindow?
     
+    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
+        handleUniversalLink(userActivity: userActivity)
+    }
+    
     func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
         // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
         // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
@@ -33,6 +37,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         #if DEBUG
         DoraemonManager.shareInstance().install()
         #endif
+        
+        if let userActivity = connectionOptions.userActivities.first {
+            handleUniversalLink(userActivity: userActivity)
+        }
     }
 
     func sceneDidDisconnect(_ scene: UIScene) {
@@ -102,4 +110,14 @@ extension SceneDelegate {
         
         LNAccountManager.shared.loginByToken()
     }
+    
+    private func handleUniversalLink(userActivity: NSUserActivity) {
+        // 验证是否为 Universal Link
+        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
+              let url = userActivity.webpageURL else {
+            return
+        }
+        
+        LNDeeplinkManager.shared.handleDeepLink(url)
+    }
 }

+ 1 - 1
Lanu/Views/Game/Skill/LNSkillUserInfoView.swift

@@ -131,7 +131,7 @@ extension LNSkillUserInfoView {
             let pasteboard = UIPasteboard.general
             pasteboard.string = curDetail.userNo
             
-            showToast(.init(key: "ID 已复制到粘贴板"))
+            showToast(.init(key: "ID 已复制到剪切板"))
         }), for: .touchUpInside)
         idView.addSubview(copyButton)
         copyButton.snp.makeConstraints { make in

+ 28 - 6
Lanu/Views/Game/Skill/LNSkillVoiceBarView.swift

@@ -14,6 +14,7 @@ class LNSkillVoiceBarView: UIView {
     private let durationLabel = UILabel()
     private var curUrl: String?
     private let playStateIc = UIImageView()
+    private let voiceWaveView = LNVoiceWaveView()
     
     override init(frame: CGRect) {
         super.init(frame: frame)
@@ -44,11 +45,27 @@ extension LNSkillVoiceBarView: LNVoicePlayerNotify {
     func onAudioStartPlay(path: String) {
         guard path == curUrl else { return }
         playStateIc.image = .init(named: "ic_voice_pause")
+        voiceWaveView.startAnimate()
     }
     
     func onAudioStopPlay(path: String) {
         guard path == curUrl else { return }
         playStateIc.image = .init(named: "ic_voice_play")
+        voiceWaveView.stopAnimate()
+        LNVoiceResourceManager.shared.getRemoteAudioDuration(urlStr: path)
+        { [weak self] duration, err in
+            guard let self else { return }
+            guard let duration, err == nil else { return }
+            guard curUrl == path else { return }
+            
+            isHidden = duration <= 0
+            durationLabel.text = "\(Int(duration.rounded()))\""
+        }
+    }
+    
+    func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
+        guard path == curUrl else { return }
+        durationLabel.text = "\(Int((total - cur).rounded()))\""
     }
 }
 
@@ -63,7 +80,11 @@ extension LNSkillVoiceBarView {
         button.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             guard let curUrl else { return }
-            LNVoicePlayer.shared.play(curUrl)
+            if LNVoicePlayer.shared.playingUrl == curUrl {
+                LNVoicePlayer.shared.stop()
+            } else {
+                LNVoicePlayer.shared.play(curUrl)
+            }
         }), for: .touchUpInside)
         addSubview(button)
         button.snp.makeConstraints { make in
@@ -86,12 +107,13 @@ extension LNSkillVoiceBarView {
             make.verticalEdges.equalToSuperview()
         }
         
-        let voice = UIImageView()
-        voice.image = .init(named: "ic_voice")
-        container.addSubview(voice)
-        voice.snp.makeConstraints { make in
+        voiceWaveView.build()
+        container.addSubview(voiceWaveView)
+        voiceWaveView.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.leading.equalTo(playStateIc.snp.trailing).offset(6)
+            make.width.equalTo(19)
+            make.height.equalTo(11)
         }
         
         durationLabel.font = .heading_h5
@@ -99,7 +121,7 @@ extension LNSkillVoiceBarView {
         container.addSubview(durationLabel)
         durationLabel.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
-            make.leading.equalTo(voice.snp.trailing).offset(4)
+            make.leading.equalTo(voiceWaveView.snp.trailing).offset(4)
             make.trailing.equalToSuperview()
         }
     }

+ 4 - 8
Lanu/Views/Order/Create/LNCreateOrderViewController.swift

@@ -17,7 +17,7 @@ extension UIView {
         navigationController?.pushViewController(vc, animated: true)
     }
     
-    func pushToCreateOrder(_ qrCode: String) {
+    func pushToCreateOrder(_ qrCode: LNQRCodeDetailResponse) {
         let vc = LNCreateOrderViewController()
         vc.loadQRData(qrCode)
         navigationController?.pushViewController(vc, animated: true)
@@ -70,13 +70,9 @@ class LNCreateOrderViewController: LNViewController {
         updateContent(detail)
     }
     
-    func loadQRData(_ data: String) {
-        LNOrderManager.shared.getQRDetail(data: data) { [weak self] res in
-            guard let self else { return }
-            guard let res else { return }
-            qrDetail = res
-            updateContent(res)
-        }
+    func loadQRData(_ data: LNQRCodeDetailResponse) {
+        qrDetail = data
+        updateContent(data)
     }
 }
 

+ 9 - 1
Lanu/Views/Order/OrderQR/LNOrderQRCodeShowView.swift

@@ -132,7 +132,13 @@ extension LNOrderQRCodeShowView {
             let generator = LNOrderShareImageGenerator()
             generator.update(skill: skill, count: self.count, image: image)
             let share = generator.generate()
-            share.saveToLibrary { success, err in }
+            share.saveToLibrary { success, err in
+                if success {
+                    showToast(.init(key: "已保存到相册"))
+                } else if let err {
+                    showToast(err.localizedDescription)
+                }
+            }
         }), for: .touchUpInside)
         addSubview(saveButton)
         saveButton.snp.makeConstraints { make in
@@ -145,6 +151,7 @@ extension LNOrderQRCodeShowView {
         let cover = UIView()
         cover.backgroundColor = .fill
         cover.layer.cornerRadius = 22
+        cover.isUserInteractionEnabled = false
         saveButton.insertSubview(cover, at: 0)
         cover.snp.makeConstraints { make in
             make.directionalEdges.equalToSuperview().inset(1.5)
@@ -159,6 +166,7 @@ extension LNOrderQRCodeShowView {
         copyButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             UIPasteboard.general.string = self.qrCodeData
+            showToast(.init(key: "已复制到剪切板"))
         }), for: .touchUpInside)
         addSubview(copyButton)
         copyButton.snp.makeConstraints { make in

+ 1 - 1
Lanu/Views/Profile/Mine/LNMineUserInfoView.swift

@@ -224,7 +224,7 @@ extension LNMineUserInfoView {
             let pasteboard = UIPasteboard.general
             pasteboard.string = myUid
             
-            showToast(.init(key: "ID 已复制到粘贴板"))
+            showToast(.init(key: "ID 已复制到剪切板"))
         }), for: .touchUpInside)
         container.addSubview(copyButton)
         copyButton.snp.makeConstraints { make in

+ 11 - 8
Lanu/Views/Profile/Post/LNPostShareImageGenerator.swift

@@ -12,6 +12,7 @@ import SnapKit
 
 class LNPostShareImageGenerator {
     fileprivate let container = UIImageView()
+    private let newsView = UIView()
     private let newsLabel = UILabel()
     
     private let nameLabel = UILabel()
@@ -34,14 +35,17 @@ class LNPostShareImageGenerator {
         genderView.update(myUserInfo.gender, myUserInfo.age)
         starLabel.text = "\(myGameMateInfo?.star ?? 0)"
         bioLabel.text = info.intro
-        
-        newsLabel.text = .init(key: "在 11.11 与「用户xxx」进行了一场「网球」比赛")
     }
     
     func setAlbum(image: UIImage) {
         postView.image = image
     }
     
+    func setOrderInfo(desc: String?) {
+        newsLabel.text = desc
+        newsView.isHidden = desc?.isEmpty != false
+    }
+    
     func setSkills(skills: [LNGameMateSkillVO]) {
         skillsView.arrangedSubviews.forEach {
             skillsView.removeArrangedSubview($0)
@@ -108,8 +112,7 @@ extension LNPostShareImageGenerator {
     }
     
     private func buildRecord() -> UIView {
-        let container = UIView()
-        container.snp.makeConstraints { make in
+        newsView.snp.makeConstraints { make in
             make.height.equalTo(32)
         }
         
@@ -119,11 +122,11 @@ extension LNPostShareImageGenerator {
         gradientLayer.locations = [0, 1]
         gradientLayer.startPoint = .init(x: 0, y: 0)
         gradientLayer.endPoint = .init(x: 1, y: 0)
-        container.layer.insertSublayer(gradientLayer, at: 0)
+        newsView.layer.insertSublayer(gradientLayer, at: 0)
         
         let ic = UIImageView()
         ic.image = .init(named: "ic_post_share_new")
-        container.addSubview(ic)
+        newsView.addSubview(ic)
         ic.snp.makeConstraints { make in
             make.leading.equalToSuperview().offset(13)
             make.centerY.equalToSuperview()
@@ -131,13 +134,13 @@ extension LNPostShareImageGenerator {
         
         newsLabel.font = .body_xs
         newsLabel.textColor = .init(hex: "#466811")
-        container.addSubview(newsLabel)
+        newsView.addSubview(newsLabel)
         newsLabel.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.leading.equalTo(ic.snp.trailing).offset(6)
         }
         
-        return container
+        return newsView
     }
     
     private func buildPost() -> UIView {

+ 22 - 12
Lanu/Views/Profile/Post/LNSharePostViewController.swift → Lanu/Views/Profile/Post/LNPostShareViewController.swift

@@ -1,5 +1,5 @@
 //
-//  LNSharePostViewController.swift
+//  LNPostShareViewController.swift
 //  Lanu
 //
 //  Created by OneeChan on 2025/11/27.
@@ -12,18 +12,19 @@ import Combine
 
 
 extension UIView {
-    func pushToSharePost() {
-        let vc = LNSharePostViewController()
+    func pushToPostShare() {
+        let vc = LNPostShareViewController()
         navigationController?.pushViewController(vc, animated: true)
     }
 }
 
 
-class LNSharePostViewController: LNViewController {
+class LNPostShareViewController: LNViewController {
     private let postView = UIImageView()
     
     private let skillStackView = UIStackView()
     private var selectedSkills: [LNGameMateSkillVO] = []
+    private var lastDesc: String?
     
     override func viewDidLoad() {
         super.viewDidLoad()
@@ -31,12 +32,12 @@ class LNSharePostViewController: LNViewController {
         setupViews()
         prefetchSkillIcon()
         
-        selectedSkills = myGameMateInfo?.skills ?? []
+        selectedSkills = Array(myGameMateInfo?.skills.prefix(3) ?? [])
         updateSkillsView()
     }
 }
 
-extension LNSharePostViewController {
+extension LNPostShareViewController {
     private func prefetchSkillIcon() {
         myGameMateInfo?.skills.forEach {
             SDWebImageManager.shared.loadImage(with: URL(string: $0.icon),
@@ -44,9 +45,17 @@ extension LNSharePostViewController {
             { _, _, _, _, _, _ in }
         }
     }
+    
+    private func loadLastOrder() {
+        LNOrderManager.shared.getLastOrder { [weak self] res in
+            guard let self else { return }
+            guard let res else { return }
+            lastDesc = .init(key: "在 %@ 与 %@ 进行了一场 %@ 比赛", Double(res.finishTime).formattedDate("."), res.nickname, res.bizCateGoryName)
+        }
+    }
 }
 
-extension LNSharePostViewController {
+extension LNPostShareViewController {
     private func updateSkillsView() {
         skillStackView.arrangedSubviews.forEach {
             skillStackView.removeArrangedSubview($0)
@@ -140,6 +149,7 @@ extension LNSharePostViewController {
             
             let generator = LNPostShareImageGenerator()
             generator.setInfo(info: info)
+            generator.setOrderInfo(desc: lastDesc)
             generator.setAlbum(image: album)
             generator.setSkills(skills: selectedSkills)
             generator.setShareQRCode(url: .profileShareUrl + "?id=\(myUid)&share=app")
@@ -276,13 +286,13 @@ extension LNSharePostViewController {
         let container = UIView()
         container.backgroundColor = .fill
         container.layer.cornerRadius = 20
-        
-        skillStackView.axis = .horizontal
-        skillStackView.spacing = 10
-        skillStackView.onTap { [weak self] in
+        container.onTap { [weak self] in
             guard let self else { return }
             showSkillSelectPanel()
         }
+        
+        skillStackView.axis = .horizontal
+        skillStackView.spacing = 10
         container.addSubview(skillStackView)
         skillStackView.snp.makeConstraints { make in
             make.leading.equalToSuperview().offset(113)
@@ -421,7 +431,7 @@ import SwiftUI
 
 struct LNSharePostViewControllerPreview: UIViewControllerRepresentable {
     func makeUIViewController(context: Context) -> some UIViewController {
-        LNSharePostViewController()
+        LNPostShareViewController()
     }
     
     func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfileNaviBarView.swift

@@ -188,7 +188,7 @@ extension LNProfileNaviBarView {
         }
         shareButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
-            pushToSharePost()
+            pushToPostShare()
         }), for: .touchUpInside)
         stackView.addArrangedSubview(shareButton)
         

+ 1 - 2
Lanu/Views/Profile/Profile/LNProfileUserDetailView.swift

@@ -44,8 +44,7 @@ class LNProfileUserDetailView: UIView {
         descLabel.superview?.isHidden = detail.intro.isEmpty
         descLabel.text = detail.intro
         
-        tagView.isHidden = true
-        //        tagView.update(detail.)
+        tagView.update(detail.interests.compactMap({ $0.name }))
         
         skillsView.update(detail.skills)
         skillsView.isHidden = !detail.playmate

+ 1 - 1
Lanu/Views/Profile/Profile/LNProfileUserInfoView.swift

@@ -101,7 +101,7 @@ extension LNProfileUserInfoView {
             let pasteboard = UIPasteboard.general
             pasteboard.string = curDetail.userNo
             
-            showToast(.init(key: "ID 已复制到粘贴板"))
+            showToast(.init(key: "ID 已复制到剪切板"))
         }), for: .touchUpInside)
         idView.addSubview(copyButton)
         copyButton.snp.makeConstraints { make in

+ 1 - 0
Lanu/Views/Profile/Profile/LNProfileViewController.swift

@@ -103,6 +103,7 @@ extension LNProfileViewController {
             cover.sd_setImage(with: URL(string: detail.cover))
         }
         userInfoView.update(detail)
+        voiceBar.update(detail.voiceBar)
         infoView.update(detail)
         bottomMenu.update(detail)
         scoreView.update(detail)

+ 25 - 1
Lanu/Views/Profile/Profile/LNProfileVoiceBarView.swift

@@ -20,10 +20,20 @@ class LNProfileVoiceBarView: UIButton {
         super.init(frame: frame)
         
         setupViews()
+        LNEventDeliver.addObserver(self)
     }
     
-    func update(_ url: String, duration: Double) {
+    func update(_ url: String) {
         curUrl = url
+        LNVoiceResourceManager.shared.getRemoteAudioDuration(urlStr: url)
+        { [weak self] duration, err in
+            guard let self else { return }
+            guard let duration, err == nil else { return }
+            guard curUrl == url else { return }
+            
+            isHidden = duration <= 0
+            durationLabel.text = "\(Int(duration.rounded()))\""
+        }
     }
     
     required init?(coder: NSCoder) {
@@ -40,6 +50,20 @@ extension LNProfileVoiceBarView: LNVoicePlayerNotify {
     func onAudioStopPlay(path: String) {
         guard path == curUrl else { return }
         statusIc.image = .init(named: "ic_voice_play")
+        LNVoiceResourceManager.shared.getRemoteAudioDuration(urlStr: path)
+        { [weak self] duration, err in
+            guard let self else { return }
+            guard let duration, err == nil else { return }
+            guard curUrl == path else { return }
+            
+            isHidden = duration <= 0
+            durationLabel.text = "\(Int(duration.rounded()))\""
+        }
+    }
+    
+    func onAudioUpdateDuration(path: String, cur: TimeInterval, total: TimeInterval) {
+        guard path == curUrl else { return }
+        durationLabel.text = "\(Int((total - cur).rounded()))\""
     }
 }