Selaa lähdekoodia

feat: 整理优化媒体选择逻辑

陈文艺 3 viikkoa sitten
vanhempi
sitoutus
4ee7eb1e91

+ 3 - 1
Lanu.xcodeproj/project.pbxproj

@@ -47,6 +47,8 @@
 				"Common/Extension/DispatchQueue+Extension.swift",
 				"Common/Extension/Double+Extension.swift",
 				"Common/Extension/Int+Extension.swift",
+				Common/Extension/LNMediaFileUtils.swift,
+				Common/Extension/LNPermissionHelper.swift,
 				"Common/Extension/NSObject+Extension.swift",
 				"Common/Extension/String+Extension.swift",
 				"Common/Extension/TimeInterval+Extension.swift",
@@ -59,7 +61,7 @@
 				"Common/Extension/UIView+Extension.swift",
 				"Common/Extension/URL+Extension.swift",
 				Common/Keyboard/LNKeyboardManager.swift,
-				Common/LNPhotosPicker.swift,
+				Common/LNMediaPicker.swift,
 				Common/Logger/LNLogger.swift,
 				Common/Logger/LNLoggerFormater.swift,
 				Common/Storage/LNUserDefaults.swift,

+ 3 - 7
Lanu/AppDelegate.swift

@@ -126,21 +126,17 @@ extension AppDelegate {
     }
     
     private func requestNotificationPermissions() {
-        let center = UNUserNotificationCenter.current()
-        
         // 请求通知权限(可根据需求调整,如添加 sound/badge 等)
-        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
+        LNPermissionHelper.requestNotificationAuthorization(options: [.alert, .sound, .badge]) { granted, error in
             if let error = error {
                 Log.e("请求推送权限失败: \(error.localizedDescription)")
                 return
             }
-            
+
             if granted {
                 Log.i("用户已授予推送权限")
                 // 权限获取成功后,注册远程推送
-                DispatchQueue.main.async {
-                    UIApplication.shared.registerForRemoteNotifications()
-                }
+                UIApplication.shared.registerForRemoteNotifications()
             } else {
                 Log.i("用户拒绝了推送权限")
             }

+ 26 - 0
Lanu/Common/Extension/LNMediaFileUtils.swift

@@ -0,0 +1,26 @@
+//
+//  LNMediaFileUtils.swift
+//  Lanu
+//
+//  Created by OneeChan on 2026/03/08.
+//
+
+import Foundation
+
+struct LNMediaFileUtils {
+    static func persistTemporaryVideo(from url: URL) -> URL? {
+        let fileManager = FileManager.default
+        let destinationUrl = fileManager.temporaryDirectory
+            .appendingPathComponent(UUID().uuidString)
+            .appendingPathExtension(url.pathExtension.isEmpty ? "mp4" : url.pathExtension)
+        do {
+            if fileManager.fileExists(atPath: destinationUrl.path) {
+                try fileManager.removeItem(at: destinationUrl)
+            }
+            try fileManager.copyItem(at: url, to: destinationUrl)
+            return destinationUrl
+        } catch {
+            return url
+        }
+    }
+}

+ 63 - 0
Lanu/Common/Extension/LNPermissionHelper.swift

@@ -0,0 +1,63 @@
+//
+//  LNPermissionHelper.swift
+//  Lanu
+//
+//  Created by OneeChan on 2026/03/08.
+//
+
+import Foundation
+import Photos
+import AVFoundation
+import AVFAudio
+import UserNotifications
+
+struct LNPermissionHelper {
+    static func requestPhotoLibraryAuthorization(_ completion: @escaping (PHAuthorizationStatus) -> Void) {
+        PHPhotoLibrary.requestAuthorization { status in
+            runOnMain {
+                completion(status)
+            }
+        }
+    }
+
+    static func cameraAuthorizationStatus() -> AVAuthorizationStatus {
+        AVCaptureDevice.authorizationStatus(for: .video)
+    }
+
+    static func requestCameraAccess(_ completion: @escaping (Bool) -> Void) {
+        AVCaptureDevice.requestAccess(for: .video) { granted in
+            runOnMain {
+                completion(granted)
+            }
+        }
+    }
+
+    static func microphonePermission() -> AVAudioSession.RecordPermission {
+        AVAudioSession.sharedInstance().recordPermission
+    }
+
+    static func requestMicrophoneAccess(_ completion: @escaping (Bool) -> Void) {
+        AVAudioSession.sharedInstance().requestRecordPermission { granted in
+            runOnMain {
+                completion(granted)
+            }
+        }
+    }
+
+    static func requestNotificationAuthorization(options: UNAuthorizationOptions,
+                                                 completion: @escaping (Bool, Error?) -> Void) {
+        UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in
+            runOnMain {
+                completion(granted, error)
+            }
+        }
+    }
+
+    static func fetchNotificationSettings(_ completion: @escaping (UNNotificationSettings) -> Void) {
+        UNUserNotificationCenter.current().getNotificationSettings { settings in
+            runOnMain {
+                completion(settings)
+            }
+        }
+    }
+}

+ 216 - 0
Lanu/Common/LNMediaPicker.swift

@@ -0,0 +1,216 @@
+//
+//  LNMediaPicker.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/11/26.
+//
+
+import Foundation
+import UIKit
+import Photos
+
+
+enum LNMediaPickerType: CaseIterable {
+    case camera
+    case photo
+    
+    var title: String {
+        switch self {
+        case .camera: .init(key: "A00011")
+        case .photo: .init(key: "A00012")
+        }
+    }
+}
+
+
+extension LNBottomSheetMenu {
+    static func showImageSelectMenu(view: UIView? = nil,
+                                    options: LNMediaPickerOptions = .init(),
+                                    handler: @escaping LNMediaPickerHandler) {
+        let panel = LNBottomSheetMenu()
+        let types: [LNMediaPickerType] = [.camera, .photo]
+        panel.update([
+            LNMediaPickerType.camera.title,
+            LNMediaPickerType.photo.title,
+            .init(key: "A00003")
+        ]) { index, _ in
+            guard index < types.count else { return }
+            let type = types[index]
+            LNMediaPicker.shared.pick(from: view, type: type, options: options, handler: handler)
+        }
+        panel.popup()
+    }
+}
+
+
+extension LNCommonAlertView {
+    static func showPhotoLibraryPermissionAlert() {
+        let alert = LNCommonAlertView()
+        alert.titleLabel.text = .init(key: "A00287")
+        alert.showConfirm(.init(key: "A00289")) {
+            openAppSetting()
+        }
+        alert.showCancel()
+        alert.popup()
+    }
+    
+    static func showCameraPermissionAlert() {
+        let alert = LNCommonAlertView()
+        alert.titleLabel.text = .init(key: "A00288")
+        alert.showConfirm(.init(key: "A00289")) {
+            openAppSetting()
+        }
+        alert.showCancel()
+        alert.popup()
+    }
+}
+
+
+typealias LNMediaPickerHandler = (_ image: UIImage?, _ videoUrl: URL?) -> Void
+
+struct LNMediaPickerOptions {
+    let allowEdit: Bool
+    let allowVideo: Bool
+    let videoMaximumDuration: TimeInterval
+
+    init(allowEdit: Bool = false,
+         allowVideo: Bool = false,
+         videoMaximumDuration: TimeInterval = 60) {
+        self.allowEdit = allowEdit
+        self.allowVideo = allowVideo
+        self.videoMaximumDuration = videoMaximumDuration
+    }
+}
+
+private var mediaHandlerKey: UInt8 = 0
+extension UIViewController {
+    fileprivate var mediaPickerHandler: LNMediaPickerHandler? {
+        get {
+            objc_getAssociatedObject(self, &mediaHandlerKey) as? LNMediaPickerHandler ?? nil
+        }
+        set {
+            objc_setAssociatedObject(self, &mediaHandlerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+}
+
+class LNMediaPicker: NSObject {
+    static let shared = LNMediaPicker()
+    
+    private override init() {
+        super.init()
+    }
+}
+
+extension LNMediaPicker {
+    func pick(from view: UIView? = nil,
+              type: LNMediaPickerType,
+              options: LNMediaPickerOptions,
+              handler: @escaping LNMediaPickerHandler) {
+        switch type {
+        case .photo:
+            handlePhotoAuthorization(view: view, options: options, handler: handler)
+        case .camera:
+            handleCameraAuthorization(view: view, options: options, handler: handler)
+        }
+    }
+
+    private func handlePhotoAuthorization(view: UIView?,
+                                          options: LNMediaPickerOptions,
+                                          handler: @escaping LNMediaPickerHandler) {
+        // 申请相册权限(iOS 10+ 需授权)
+        LNPermissionHelper.requestPhotoLibraryAuthorization { [weak self] status in
+            guard let self else { return }
+            switch status {
+            case .authorized: // 已授权,打开相册
+                self.presentPicker(from: view, sourceType: .photoLibrary, options: options, handler: handler)
+            case .denied, .restricted: // 拒绝授权或受限制
+                LNCommonAlertView.showPhotoLibraryPermissionAlert()
+                handler(nil, nil)
+            case .notDetermined: // 首次请求,系统会自动弹出授权框(无需额外处理)
+                break
+            case .limited:
+                break
+            @unknown default:
+                break
+            }
+        }
+    }
+
+    private func handleCameraAuthorization(view: UIView?,
+                                           options: LNMediaPickerOptions,
+                                           handler: @escaping LNMediaPickerHandler) {
+        let status = LNPermissionHelper.cameraAuthorizationStatus()
+
+        switch status {
+        case .authorized: // 已授权
+            presentPicker(from: view, sourceType: .camera, options: options, handler: handler)
+        case .notDetermined: // 未决定,请求权限
+            LNPermissionHelper.requestCameraAccess { [weak self] granted in
+                guard let self else { return }
+                guard granted else {
+                    handler(nil, nil)
+                    return
+                }
+                self.presentPicker(from: view, sourceType: .camera, options: options, handler: handler)
+            }
+        case .denied, .restricted: // 拒绝/受限
+            LNCommonAlertView.showCameraPermissionAlert()
+            handler(nil, nil)
+        @unknown default:
+            handler(nil, nil)
+        }
+    }
+}
+
+extension LNMediaPicker {
+    private func presentPicker(from view: UIView?,
+                               sourceType: UIImagePickerController.SourceType,
+                               options: LNMediaPickerOptions,
+                               handler: @escaping LNMediaPickerHandler) {
+        let vc = view?.viewController ?? UIView.appKeyWindow?.rootViewController
+        let picker = UIImagePickerController()
+        picker.delegate = self
+        picker.sourceType = sourceType
+        picker.allowsEditing = options.allowEdit
+        picker.mediaTypes = options.allowVideo
+        ? [UTType.image.identifier, UTType.movie.identifier]
+        : [UTType.image.identifier]
+        if sourceType == .camera {
+            picker.videoMaximumDuration = options.videoMaximumDuration
+        }
+        picker.mediaPickerHandler = handler
+        vc?.present(picker, animated: true)
+    }
+}
+
+extension LNMediaPicker: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
+        // 关闭相册
+        picker.dismiss(animated: true)
+        
+        if let videoURL = info[.mediaURL] as? URL {
+            picker.mediaPickerHandler?(nil, videoURL)
+            return
+        }
+        
+        // 获取选择的图片(editedImage:编辑后的图片,originalImage:原始图片)
+        let image: UIImage?
+        if let editedImage = info[.editedImage] as? UIImage {
+            image = editedImage
+        } else if let originalImage = info[.originalImage] as? UIImage {
+            image = originalImage
+        } else {
+            image = nil
+        }
+        picker.mediaPickerHandler?(image, nil)
+    }
+    
+    // 取消选择
+    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
+        picker.dismiss(animated: true)
+        picker.mediaPickerHandler?(nil, nil)
+    }
+}
+
+

+ 0 - 237
Lanu/Common/LNPhotosPicker.swift

@@ -1,237 +0,0 @@
-//
-//  LNPhotosPicker.swift
-//  Lanu
-//
-//  Created by OneeChan on 2025/11/26.
-//
-
-import Foundation
-import UIKit
-import Photos
-
-
-enum LNImagePickerType: CaseIterable {
-    case camera
-    case photo
-    
-    var title: String {
-        switch self {
-        case .camera: .init(key: "A00011")
-        case .photo: .init(key: "A00012")
-        }
-    }
-}
-
-
-extension LNBottomSheetMenu {
-    static func showImageSelectMenu(view: UIView? = nil,
-                                    allowEdit: Bool = false,
-                                    video: Bool = false,
-                                    handler: @escaping LNMediaPickerHandler) {
-        let panel = LNBottomSheetMenu()
-        panel.update([
-            LNImagePickerType.camera.title,
-            LNImagePickerType.photo.title,
-            .init(key: "A00003")
-        ]) { index, _ in
-            if index == 0 {
-                LNMediaPicker.shared.takePictures(from: view, allowEdit: allowEdit, video: video, handler: handler)
-            } else if index == 1 {
-                LNMediaPicker.shared.selectPhoto(from: view, allowEdit: allowEdit, video: video, handler: handler)
-            }
-        }
-        panel.popup()
-    }
-}
-
-
-extension LNCommonAlertView {
-    static func showPhotoLibraryPermissionAlert() {
-        let alert = LNCommonAlertView()
-        alert.titleLabel.text = .init(key: "A00287")
-        alert.showConfirm(.init(key: "A00289")) {
-            openAppSetting()
-        }
-        alert.showCancel()
-        alert.popup()
-    }
-    
-    static func showCameraPermissionAlert() {
-        let alert = LNCommonAlertView()
-        alert.titleLabel.text = .init(key: "A00288")
-        alert.showConfirm(.init(key: "A00289")) {
-            openAppSetting()
-        }
-        alert.showCancel()
-        alert.popup()
-    }
-}
-
-
-typealias LNMediaPickerHandler = (_ image: UIImage?, _ videoUrl: URL?) -> Void
-private class LNImagePickerController: UIImagePickerController {
-    var handler: LNMediaPickerHandler?
-}
-
-class LNMediaPicker: NSObject {
-    static let shared = LNMediaPicker()
-    
-    private override init() {
-        super.init()
-    }
-}
-
-extension LNMediaPicker {
-    func selectPhoto(from view: UIView? = nil,
-                     allowEdit: Bool = false,
-                     video: Bool = false,
-                     handler: @escaping LNMediaPickerHandler) {
-        // 2. 申请相册权限(iOS 10+ 需授权)
-        PHPhotoLibrary.requestAuthorization { [weak self, weak view] status in
-            guard let self, let view else { return }
-            DispatchQueue.main.async { [weak self, weak view] in
-                guard let self, let view else { return }
-                
-                switch status {
-                case .authorized: // 已授权,打开相册
-                    self.openPhotoLibrary(view, allowEdit: allowEdit, video: video, handler: handler)
-                case .denied, .restricted: // 拒绝授权或受限制
-                    LNCommonAlertView.showPhotoLibraryPermissionAlert()
-                    handler(nil, nil)
-                    break
-                case .notDetermined: // 首次请求,系统会自动弹出授权框(无需额外处理)
-                    break
-                case .limited:
-                    break
-                @unknown default:
-                    break
-                }
-            }
-        }
-    }
-    
-    func takePictures(from view: UIView? = nil,
-                      allowEdit: Bool = false,
-                      video: Bool = false,
-                      handler: @escaping LNMediaPickerHandler) {
-        let status = AVCaptureDevice.authorizationStatus(for: .video)
-        
-        switch status {
-        case .authorized: // 已授权
-            self.openCamera(view, allowEdit: allowEdit, video: video, handler: handler)
-        case .notDetermined: // 未决定,请求权限
-            AVCaptureDevice.requestAccess(for: .video) { [weak self, weak view] granted in
-                guard let self, let view else { return }
-                DispatchQueue.main.async { [weak self, weak view] in
-                    guard let self, let view else { return }
-                    guard granted else {
-                        handler(nil, nil)
-                        return
-                    }
-                    
-                    self.openCamera(view, allowEdit: allowEdit, video: video, handler: handler)
-                }
-            }
-        case .denied, .restricted: // 拒绝/受限
-            LNCommonAlertView.showCameraPermissionAlert()
-            handler(nil, nil)
-        @unknown default:
-            handler(nil, nil)
-        }
-    }
-}
-
-extension LNMediaPicker {
-    private func buildSelectPhotoPicker(
-        allowEdit: Bool,
-        video: Bool,
-        handler: @escaping LNMediaPickerHandler
-    ) -> UIImagePickerController {
-        let vc = LNImagePickerController()
-        vc.delegate = self
-        vc.sourceType = .photoLibrary
-        vc.allowsEditing = allowEdit
-        if video {
-            vc.mediaTypes = [UTType.image.identifier, UTType.movie.identifier]
-        } else {
-            vc.mediaTypes = [UTType.image.identifier]
-        }
-        vc.videoMaximumDuration = 60
-        vc.handler = handler
-        
-        return vc
-    }
-    
-    private func openPhotoLibrary(_ view: UIView?,
-                                  allowEdit: Bool,
-                                  video: Bool,
-                                  handler: @escaping LNMediaPickerHandler) {
-        let vc = view?.viewController ?? UIView.appKeyWindow?.rootViewController
-        let picker = buildSelectPhotoPicker(allowEdit: allowEdit, video: video, handler: handler)
-        vc?.present(picker, animated: true)
-    }
-}
- 
-extension LNMediaPicker {
-    private func buildTakePicturesPicker(
-        allowEdit: Bool,
-        video: Bool,
-        handler: @escaping LNMediaPickerHandler
-    ) -> UIImagePickerController {
-        let vc = LNImagePickerController()
-        vc.delegate = self
-        vc.sourceType = .camera
-        vc.allowsEditing = allowEdit
-        if video {
-            vc.mediaTypes = [UTType.image.identifier, UTType.movie.identifier]
-        } else {
-            vc.mediaTypes = [UTType.image.identifier]
-        }
-        vc.videoMaximumDuration = 60
-        vc.handler = handler
-        
-        
-        return vc
-    }
-    
-    private func openCamera(_ view: UIView?,
-                            allowEdit: Bool,
-                            video: Bool,
-                            handler: @escaping LNMediaPickerHandler) {
-        let vc = view?.viewController ?? UIView.appKeyWindow?.rootViewController
-        let picker = buildTakePicturesPicker(allowEdit: allowEdit, video: video, handler: handler)
-        vc?.present(picker, animated: true)
-    }
-}
-
-extension LNMediaPicker: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
-    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
-        guard let vc = picker as? LNImagePickerController else { return }
-        // 关闭相册
-        picker.dismiss(animated: true)
-        
-        if let videoURL = info[.mediaURL] as? URL {
-            vc.handler?(nil, videoURL)
-            return
-        }
-        
-        // 获取选择的图片(editedImage:编辑后的图片,originalImage:原始图片)
-        let image: UIImage?
-        if let editedImage = info[.editedImage] as? UIImage {
-            image = editedImage
-        } else if let originalImage = info[.originalImage] as? UIImage {
-            image = originalImage
-        } else {
-            image = nil
-        }
-        vc.handler?(image, nil)
-    }
-    
-    // 取消选择
-    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
-        guard let vc = picker as? LNImagePickerController else { return }
-        
-        picker.dismiss(animated: true)
-        vc.handler?(nil, nil)
-    }
-}

+ 5 - 5
Lanu/Common/Views/LNVideoPlayerView.swift

@@ -100,13 +100,13 @@ class LNVideoPlayerView: UIView {
                 do {
                     let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
                     let thumbnailImage = UIImage(cgImage: cgImage)
-                    DispatchQueue.main.async { [weak self] in
-                        guard let self else { return }
-                        guard curSource == url else { return }
-                        coverImageView.image = thumbnailImage
+                    DispatchQueue.main.async {
+                        self.coverImageView.image = thumbnailImage
                     }
                 } catch {
-                    coverImageView.image = UIImage(systemName: "film")
+                    DispatchQueue.main.async {
+                        self.coverImageView.image = UIImage(systemName: "film")
+                    }
                 }
             }
             

+ 19 - 41
Lanu/Common/Voice/LNVoiceRecorder.swift

@@ -49,51 +49,29 @@ class LNVoiceRecorder {
     
     private init() { }
     
-    func requestMicrophonePermission(handler: @escaping (Bool) -> Void) {
-        if AVAudioSession.sharedInstance().recordPermission == .granted {
-            handler(true)
+    func startRecord(_ maxDuration: Double? = nil, handler: @escaping (String?) -> Void) {
+        guard curState == .ready,
+              setupNewRecorder(),
+              recorder?.record() == true
+        else {
+            handler(nil)
             return
         }
-        
-        AVAudioSession.sharedInstance().requestRecordPermission { granted in
-            if !granted {
-                showToast(.init(key: "B00022"))
-            }
-            DispatchQueue.main.async {
-                handler(granted)
-            }
+        if let maxDuration {
+            self.maxDuration = maxDuration
+        } else {
+            self.maxDuration = -1
         }
-    }
-    
-    func startRecord(_ maxDuration: Double? = nil, handler: @escaping (String?) -> Void) {
-        requestMicrophonePermission { [weak self] granted in
-            guard let self, granted else {
-                handler(nil)
-                return
-            }
-            guard curState == .ready,
-                  setupNewRecorder(),
-                  recorder?.record() == true
-            else {
-                handler(nil)
-                return
-            }
-            if let maxDuration {
-                self.maxDuration = maxDuration
-            } else {
-                self.maxDuration = -1
-            }
-            
-            startDurationTimer()
-            curState = .recording
-            curTaskId = "\(curTime)"
-            
-            DispatchQueue.main.async { [weak self] in
-                guard let self else { return }
-                notifyTaskStart()
-            }
-            handler(curTaskId)
+        
+        startDurationTimer()
+        curState = .recording
+        curTaskId = "\(curTime)"
+        
+        DispatchQueue.main.async { [weak self] in
+            guard let self else { return }
+            notifyTaskStart()
         }
+        handler(curTaskId)
     }
     
     func pauseRecord() {

+ 5 - 2
Lanu/Manager/IM/LNIMManager.swift

@@ -279,9 +279,12 @@ extension LNIMManager {
             return
         }
         
-        LNVoiceRecorder.shared.requestMicrophonePermission { [weak self] succes in
+        LNPermissionHelper.requestMicrophoneAccess { [weak self] granted in
             guard let self else { return }
-            guard succes else { return }
+            guard granted else {
+                showToast(.init(key: "B00022"))
+                return
+            }
             guard curCallInfo == nil else { return }
             
             curCallInfo = .init(uid: uid)

+ 1 - 1
Lanu/Views/Game/Join/Input/BaseInfo/LNJoinUsInputInfoView.swift

@@ -242,7 +242,7 @@ extension LNJoinUsInputInfoView {
         avatar.delegate = self
         avatar.onTap { [weak self] in
             guard let self else { return }
-            LNBottomSheetMenu.showImageSelectMenu(view: self, allowEdit: true)
+            LNBottomSheetMenu.showImageSelectMenu(view: self, options: .init(allowEdit: true))
             { [weak self] image, _ in
                 guard let self else { return }
                 guard let image = image?.compress(type: .avatar) else { return }

+ 1 - 1
Lanu/Views/IM/Chat/InputMenu/LNIMChatInputMenuView.swift

@@ -42,7 +42,7 @@ extension LNIMChatInputMenuView: LNIMChatTextInputViewDelegate {
     func onVoiceInputClick() {
         endEditing(true)
         
-        LNVoiceRecorder.shared.requestMicrophonePermission { [weak self] granted in
+        LNPermissionHelper.requestMicrophoneAccess { [weak self] granted in
             guard let self else { return }
             guard granted else {
                 showToast(.init(key: "B00022"))

+ 14 - 20
Lanu/Views/IM/ConversationList/LNIMNotificationPermissionView.swift

@@ -25,34 +25,28 @@ class LNIMNotificationPermissionView: UIView {
 
 extension LNIMNotificationPermissionView {
     private func requestPermission() {
-        let center = UNUserNotificationCenter.current()
-        center.requestAuthorization(options: [.alert, .sound, .badge])
-        { [weak self] granted, error in
+        LNPermissionHelper.requestNotificationAuthorization(options: [.alert, .sound, .badge])
+        { [weak self] granted, _ in
             guard let self else { return }
             guard granted else { return }
-            DispatchQueue.main.async {
-                UIApplication.shared.registerForRemoteNotifications()
-            }
+            UIApplication.shared.registerForRemoteNotifications()
             checkPermission()
         }
     }
     
     private func checkPermission() {
-        UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in
+        LNPermissionHelper.fetchNotificationSettings { [weak self] settings in
             guard let self else { return }
-            DispatchQueue.main.async { [weak self] in
-                guard let self else { return }
-                switch settings.authorizationStatus {
-                case .authorized, .provisional, .ephemeral:
-                    isHidden = true
-                    break
-                case .notDetermined:
-                    requestPermission()
-                    isHidden = false
-                default:
-                    isHidden = false
-                    break
-                }
+            switch settings.authorizationStatus {
+            case .authorized, .provisional, .ephemeral:
+                isHidden = true
+                break
+            case .notDetermined:
+                requestPermission()
+                isHidden = false
+            default:
+                isHidden = false
+                break
             }
         }
     }

+ 2 - 2
Lanu/Views/Login/Setup/LNBaseInfoSetupViewController.swift

@@ -87,9 +87,9 @@ extension LNBaseInfoSetupViewController {
         panel.update(titles) { [weak self] index, text in
             guard let self else { return }
             if index == 0 {
-                LNMediaPicker.shared.takePictures(from: view, allowEdit: true, handler: handler)
+                LNMediaPicker.shared.pick(from: view, type: .camera, options: .init(allowEdit: true), handler: handler)
             } else if index == 1 {
-                LNMediaPicker.shared.selectPhoto(from: view, allowEdit: true, handler: handler)
+                LNMediaPicker.shared.pick(from: view, type: .photo, options: .init(allowEdit: true), handler: handler)
             } else if index == 2 {
                 guard let item = randomProfile?.avatars.randomElement() else { return }
                 avatar.loadImage(url: item)

+ 1 - 1
Lanu/Views/Profile/Edit/LNEditProfileViewController.swift

@@ -283,7 +283,7 @@ extension LNEditProfileViewController {
         profilePhoto.delegate = self
         profilePhoto.onTap { [weak self] in
             guard let self else { return }
-            LNBottomSheetMenu.showImageSelectMenu(view: view, allowEdit: true)
+            LNBottomSheetMenu.showImageSelectMenu(view: view, options: .init(allowEdit: true))
             { [weak self] image, _ in
                 guard let self else { return }
                 guard let image = image?.compress(type: .avatar) else { return }

+ 3 - 2
Lanu/Views/Profile/Feed/LNCreateFeedViewController.swift

@@ -190,7 +190,7 @@ extension LNCreateFeedViewController {
         selectPhotoView.layer.cornerRadius = 8.33
         selectPhotoView.onTap { [weak self] in
             guard let self else { return }
-            LNBottomSheetMenu.showImageSelectMenu(view: view, video: photosView.arrangedSubviews.count == 1)
+            LNBottomSheetMenu.showImageSelectMenu(view: view, options: .init(allowVideo: photosView.arrangedSubviews.count == 1))
             { [weak self] image, videoUrl in
                 guard let self else { return }
                 if let image {
@@ -273,7 +273,8 @@ extension LNCreateFeedViewController {
         videoUploadView.delegate = self
         videoUploadView.onTap { [weak self] in
             guard let self else { return }
-            view.presentVideoPreview([videoUploadView.sourceUrl!.absoluteString], 0)
+            guard let sourceUrl = videoUploadView.sourceUrl else { return }
+            view.presentVideoPreview([sourceUrl.absoluteString], 0)
         }
         videoUploadView.snp.makeConstraints { make in
             make.size.equalTo(CGSize.zero)