Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/Threema/Utilities/MWWrapper/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 13 kB image not shown  

Quelle  MWPhotoBrowserWrapper.swift   Sprache: JAVA

 

import AudioSessionManager
import CocoaLumberjackSwift
import Foundation
import SwiftUI
import ThreemaFramework
import ThreemaMacros
import UIKit

protocol MWPhotoBrowserWrapperDelegate: AnyObject {
    /// Called before the browser deletes messages
    /// - Parameter objectIDs: Object IDs of messages that will be deleted
    func willDeleteMessages(with objectIDs: [NSManagedObjectID])
}

final class MWPhotoBrowserWrapper: NSObject, MWPhotoBrowserDelegate, MWVideoDelegate, MWFileDelegate,
    ModalNavigationControllerDelegate {

    let conversation: ConversationEntity
    weak var parentViewController: UIViewController?
    let entityManager: EntityManager
    
    private weak var delegate: MWPhotoBrowserWrapperDelegate?

    private let audioSessionManager: any AudioSessionManagerProtocol

    /// Held from the first video played until the browser goes away.
    private var audioSessionClaim: AudioSessionClaim?

    // MARK: - Lifecycle
    
    init(
        for conversation: ConversationEntity,
        in parentViewController: UIViewController?,
        entityManager: EntityManager,
        delegate: MWPhotoBrowserWrapperDelegate,
        audioSessionManager: any AudioSessionManagerProtocol = BusinessInjector.ui.audioSessionManager
    ) {
        self.conversation = conversation
        self.parentViewController = parentViewController
        self.entityManager = entityManager
        self.delegate = delegate
        self.audioSessionManager = audioSessionManager

        super.init()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(colorThemeChanged(notification:)),
            name: NSNotification.Name(rawValue: kNotificationColorThemeChanged),
            object: nil
        )
    }
    
    deinit {
        guard let claim = audioSessionClaim else {
            return
        }

        // Copied into a local: the `Task` outlives this object.
        let manager = audioSessionManager
        Task {
            await manager.release(claim)
        }
    }

    private lazy var photoBrowser: MWPhotoBrowser? = createPhotoBrowser()
    
    private var mediaMessages = [BaseMessageEntity]()
    private var selectedMediaMessages = Set<UInt>()
    
    func openPhotoBrowser(for message: BaseMessageEntity?) {
        guard let message else {
            return
        }
        prepareMedia()
        
        guard let index = mediaMessages.firstIndex(of: message) else {
            DDLogError("Tapped media not found in fetched ones.")
            return
        }
        
        var autoPlayOnAppear = false
        if let fileMessageProvider = message as? FileMessageProvider {
            switch fileMessageProvider.fileMessageType {
            case .video:
                autoPlayOnAppear = true
            default:
                autoPlayOnAppear = false
            }
        }
        
        openPhotoBrowser(currentMediaIndex: UInt(index), showGrid: false, autoPlayOnAppear: autoPlayOnAppear)
    }
    
    func openPhotoBrowser() {
        openPhotoBrowser(currentMediaIndex: nil, showGrid: true)
    }
    
    // MARK: - MWPhotoBrowserDelegate

    func numberOfPhotos(in photoBrowser: MWPhotoBrowser!) -> UInt {
        UInt(mediaMessages.count)
    }
    
    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, photoAt index: UInt) -> MWPhotoProtocol! {
        photoAtIndex(at: Int(index), thumbnail: false)
    }
    
    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, objectIDAt index: UInt) -> NSManagedObjectID! {
        objectIDAtIndex(at: Int(index), thumbnail: false)
    }
    
    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, thumbPhotoAt index: UInt) -> MWPhotoProtocol! {
        let media = photoAtIndex(at: Int(index), thumbnail: true)
        media?.loadUnderlyingImageAndNotify()
        
        return media
    }
    
    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, captionViewForPhotoAt index: UInt) -> MWCaptionView! {
        guard let media = photoBrowser.photo(at: index) else {
            return PhotoCaptionView()
        }
        
        switch media {
        case let photo as MediaBrowserPhoto:
            return PhotoCaptionView(photo: photo)
            
        case let video as MediaBrowserVideo:
            return VideoCaptionView(photo: video)
            
        case let file as MediaBrowserFile:
            return FileCaptionView(photo: file)
            
        default:
            DDLogError("Could not create caption view for photo browser")
            fatalError()
        }
    }
    
    // MARK: Deleting

    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, deleteButton: UIBarButtonItem!, pressedForPhotoAt index: UInt) {
        
        let media = photoBrowser.photo(at: index)
        let deleteButtonTitle: String
        
        switch media {
        case _ as MediaBrowserPhoto:
            deleteButtonTitle = #localize("delete_photo")
            
        case _ as MediaBrowserVideo:
            deleteButtonTitle = #localize("delete_video")

        case _ as MediaBrowserFile:
            deleteButtonTitle = #localize("delete_file")

        default:
            DDLogError("Could not create caption view for photo browser")
            fatalError()
        }
        
        UIAlertTemplate.showDestructiveAlert(
            owner: photoBrowser,
            title: deleteButtonTitle,
            message: nil,
            titleDestructive: #localize("delete")
        ) { _ in
            let indexSet: Set<UInt> = [index]
            self.deleteMedia(for: indexSet) {
                self.prepareMedia()
                
                if self.mediaMessages.isEmpty {
                    self.parentViewController?.dismiss(animated: true)
                }
                else {
                    self.photoBrowser?.reloadData(true)
                }
            }
        }
    }
    
    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, deleteButton: UIBarButtonItem!) {
        if !selectedMediaMessages.isEmpty {
            deleteMedia(for: selectedMediaMessages) {
                self.handleDeletion()
            }
        }
        else {
            let indexes = Set(0..<mediaMessages.count) as! Set<UInt>
            deleteMedia(for: indexes) {
                self.handleDeletion()
            }
        }
    }
    
    // MARK: Selected

    func photoBrowserResetSelection(_ photoBrowser: MWPhotoBrowser!) {
        selectedMediaMessages.removeAll()
    }
    
    func photoBrowserSelectAll(_ photoBrowser: MWPhotoBrowser!) {
        selectedMediaMessages.removeAll()
        let setOfSelectedIndexes = Set(0..<mediaMessages.count)
        selectedMediaMessages = Set(setOfSelectedIndexes.compactMap {
            UInt($0)
        })
    }
    
    @objc func mediaPhotoSelection() -> Set<AnyHashable>! {
        selectedMediaMessages
    }
    
    @objc func mediaSelectionCount() -> UInt {
        UInt(selectedMediaMessages.count)
    }
    
    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, isPhotoSelectedAt index: UInt) -> Bool {
        selectedMediaMessages.contains(index)
    }
    
    func photoBrowser(_ photoBrowser: MWPhotoBrowser!, photoAt index: UInt, selectedChanged selected: Bool) {
        if selected {
            selectedMediaMessages.insert(index)
        }
        else {
            selectedMediaMessages.remove(index)
        }
    }
    
    // MARK: MWVideoDelegate

    func play(_ video: MediaBrowserVideo!) {
        acquireAudioSessionClaim()
    }
    
    // MARK: MWFileDelegate

    func showFile(_ fileMessageEntity: FileMessageEntity) {
        let controller = UIHostingController(rootView: FileMessagePreviewView(fileMessageEntity: fileMessageEntity))
        controller.modalPresentationStyle = .overFullScreen
        photoBrowser?.present(controller, animated: true)
    }
    
    func playFileVideo(_ fileMessageEntity: FileMessageEntity) {
        acquireAudioSessionClaim()
    }

    // MARK: - Audio session claim

    private func acquireAudioSessionClaim() {
        guard audioSessionClaim == nil else {
            return
        }

        Task { [weak self] in
            let claim = try? await self?.audioSessionManager.acquire(.mediaPlayback)

            guard let self, let claim else {
                return
            }

            audioSessionClaim = claim
        }
    }
    
    func toggleControls() {
        photoBrowser?.toggleControls()
    }
    
    // MARK: - Helpers

    private func createPhotoBrowser() -> MWPhotoBrowser? {
        let photoBrowser = MWPhotoBrowser(delegate: self)
        
        photoBrowser?.displayDeleteButton = true
        photoBrowser?.zoomPhotosToFill = true
        
        return photoBrowser
    }
    
    private func photoAtIndex(at index: Int, thumbnail: Bool) -> MWPhotoProtocol? {
       
        guard index < mediaMessages.count else {
            return nil
        }
        
        let message: BaseMessageEntity = mediaMessages[index]
        
        switch message {
        case let imageMessageEntity as ImageMessageEntity:
            let photo = MediaBrowserPhoto(imageMessageEntity: imageMessageEntity, thumbnail: thumbnail)
            photo?.caption = DateFormatter.shortStyleDateTime(message.remoteSentDate)
            return photo
            
        case let videoMessageEntity as VideoMessageEntity:
            let video = MediaBrowserVideo(thumbnail: videoMessageEntity.thumbnail?.uiImage())
            video?.delegate = self
            video?.sourceReference = videoMessageEntity
            video?.caption = DateFormatter.shortStyleDateTime(message.remoteSentDate)
            return video
            
        case let fileMessageEntity as FileMessageEntity:
            let file = MediaBrowserFile(fileMessageEntity: fileMessageEntity, thumbnail: thumbnail)
            file?.delegate = self
            file?.caption = DateFormatter.shortStyleDateTime(message.remoteSentDate)
            return file
            
        default:
            return nil
        }
    }
    
    private func objectIDAtIndex(at index: Int, thumbnail: Bool) -> NSManagedObjectID? {
       
        guard index < mediaMessages.count else {
            return nil
        }
        
        let message: BaseMessageEntity = mediaMessages[index]
        return message.objectID
    }
    
    func openPhotoBrowser(currentMediaIndex: UInt?, showGrid: Bool = true, autoPlayOnAppear: Bool = false) {
        
        photoBrowser = createPhotoBrowser()
        
        guard let photoBrowser else {
            DDLogError("Could not create MWPhotoBrowser")
            return
        }

        if currentMediaIndex == nil {
            prepareMedia()
        }
        
        photoBrowser.autoPlayOnAppear = autoPlayOnAppear
        photoBrowser.startOnGrid = showGrid
        
        // Set opening index
        if let index = currentMediaIndex {
            photoBrowser.setCurrentPhotoIndex(index)
        }
        else {
            photoBrowser.setCurrentPhotoIndex(UInt(mediaMessages.count))
        }

        // Open photo browser
        let navCon = ModalNavigationController(navigationBarClass: StatusNavigationBar.self, toolbarClass: nil)
        navCon.setViewControllers([photoBrowser], animated: false)
        navCon.modalDelegate = self
        if UIDevice.current.userInterfaceIdiom == .pad {
            navCon.modalPresentationStyle = .overFullScreen
            navCon.showFullScreenOnIPad = true
        }
        parentViewController?.present(navCon, animated: true)
    }
    
    func prepareMedia() {
        var finalMessages = [BaseMessageEntity]()
        
        let imageMessages = entityManager.entityFetcher.imageMessages(for: conversation) ?? [ImageMessage]()
        let videoMessages = entityManager.entityFetcher.videoMessages(for: conversation) ?? [VideoMessage]()
        
        let gifMimeType = UTIConverter.mimeType(fromUTI: UTType.gif.identifier)
        let audioMimeTypes = UTIConverter.renderingAudioMimeTypes()
        
        var filters = audioMimeTypes
        if let gifMimeType {
            filters.append(gifMimeType)
        }

        let fileMessages = entityManager.entityFetcher.fileMessagesForPhotoBrowser(
            for: conversation,
            filteredMimeTypes: filters
        ) ?? []
        
        finalMessages.append(contentsOf: imageMessages)
        finalMessages.append(contentsOf: videoMessages)
        finalMessages.append(contentsOf: fileMessages)

        mediaMessages = finalMessages.sorted { $0.date ?? .now < $1.date ?? .now }
    }
    
    private func deleteMedia(for indexes: Set<UInt>, completion: @escaping () -> Void) {
        let mediaEntitiesToDelete = indexes.map { index in
            self.mediaMessages[Int(index)]
        }
        
        delegate?.willDeleteMessages(with: mediaEntitiesToDelete.map(\.objectID))
        
        entityManager.performAndWaitSave {
            for mediaEntity in mediaEntitiesToDelete {
                self.entityManager.entityDestroyer.delete(baseMessage: mediaEntity)
            }
            completion()
        }
    }
    
    private func handleDeletion() {
        prepareMedia()
        
        if mediaMessages.isEmpty {
            parentViewController?.dismiss(animated: true)
        }
        else {
            photoBrowser?.finishedDeleteMedia()
        }
    }
    
    // MARK: - Notifications
    
    @objc func colorThemeChanged(notification: NSNotification) {
        photoBrowser?.reloadData(true)
    }
    
    // MARK: - ModalNavigationControllerDelegate

    func didDismissModalNavigationController() {
        guard parentViewController?.navigationController?.presentedViewController != nil else {
            photoBrowser = nil
            return
        }
    }
}

Messung
CHZG
829410092

¤ Dauer der Verarbeitung: 0.5 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.