From 1156af3d4c5215f475283d90566fc78f976da95d Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 21 Jun 2021 01:38:13 +0800 Subject: [PATCH] feat: add multiplex image nodes to StatusNode with progress loading supports --- .../Share/View/Node/Status/StatusNode.swift | 65 ++++++++++++++++++- .../ViewModel/MosaicImageViewModel.swift | 9 ++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift index c4ec3e680..9ddbcf87b 100644 --- a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift +++ b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift @@ -10,6 +10,7 @@ import Combine import AsyncDisplayKit import CoreDataStack import ActiveLabel +import func AVFoundation.AVMakeRect protocol StatusNodeDelegate: AnyObject { func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) @@ -20,6 +21,7 @@ final class StatusNode: ASCellNode { var disposeBag = Set() var timestamp: Date var timestampSubscription: AnyCancellable? + weak var delegate: StatusNodeDelegate? // needs assign on main queue static let avatarImageSize = CGSize(width: 42, height: 42) @@ -51,7 +53,6 @@ final class StatusNode: ASCellNode { // node.shouldRenderProgressImages = true return node }() - let nameTextNode = ASTextNode() let nameDotTextNode = ASTextNode() let dateTextNode = ASTextNode() @@ -62,10 +63,29 @@ final class StatusNode: ASCellNode { return node }() + let mosaicImageViewModel: MosaicImageViewModel + let mediaMultiplexImageNodes: [ASMultiplexImageNode] + init(status: Status) { timestamp = (status.reblog ?? status).createdAt + let _mosaicImageViewModel: MosaicImageViewModel = { + let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } + return MosaicImageViewModel(mediaAttachments: mediaAttachments) + }() + mosaicImageViewModel = _mosaicImageViewModel + mediaMultiplexImageNodes = { + var imageNodes: [ASMultiplexImageNode] = [] + for _ in 0..<_mosaicImageViewModel.metas.count { + let imageNode = ASMultiplexImageNode() // TODO: adapt downloader + imageNode.downloadsIntermediateImages = true + imageNode.imageIdentifiers = ["url", "previewURL"].map { $0 as NSString } // quality in descending order + imageNodes.append(imageNode) + } + return imageNodes + }() super.init() + print("meta: \(mosaicImageViewModel.metas.count), nodes: \(mediaMultiplexImageNodes.count)") automaticallyManagesSubnodes = true if let url = (status.reblog ?? status).author.avatarImageURL() { @@ -98,6 +118,10 @@ final class StatusNode: ASCellNode { ) { statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance) } + + for imageNode in mediaMultiplexImageNodes { + imageNode.dataSource = self + } } override func didEnterDisplayState() { @@ -112,6 +136,7 @@ final class StatusNode: ASCellNode { ]) } + // FIXME: needs move to other only once called callback in life cycle like: `viewDidLoad` statusContentTextNode.textView.isEditable = false statusContentTextNode.textView.textDragInteraction?.isEnabled = false statusContentTextNode.textView.linkTextAttributes = [ @@ -153,16 +178,34 @@ final class StatusNode: ASCellNode { let verticalStack = ASStackLayoutSpec.vertical() verticalStack.spacing = 10 - verticalStack.children = [ + var verticalStackChildren: [ASLayoutElement] = [ headerStack, statusContentTextNode, ] + if !mediaMultiplexImageNodes.isEmpty { + for (imageNode, meta) in zip(mediaMultiplexImageNodes, mosaicImageViewModel.metas) { + imageNode.style.preferredSize = AVMakeRect(aspectRatio: meta.size, insideRect: CGRect(origin: .zero, size: constrainedSize.max)).size + let layout = ASRatioLayoutSpec(ratio: meta.size.height / meta.size.width, child: imageNode) + verticalStackChildren.append(layout) + } + } + verticalStack.children = verticalStackChildren return verticalStack } } +//extension StatusNode: ASImageDownloaderProtocol { +// func downloadImage(with URL: URL, callbackQueue: DispatchQueue, downloadProgress: ASImageDownloaderProgress?, completion: @escaping ASImageDownloaderCompletion) -> Any? { +// +// } +// +// func cancelImageDownload(forIdentifier downloadIdentifier: Any) { +// +// } +//} + // MARK: - ASEditableTextNodeDelegate extension StatusNode: ASMetaEditableTextNodeDelegate { func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { @@ -175,3 +218,21 @@ extension StatusNode: ASMetaEditableTextNodeDelegate { return false } } + +// MARK: - ASMultiplexImageNodeDataSource +extension StatusNode: ASMultiplexImageNodeDataSource { + func multiplexImageNode(_ imageNode: ASMultiplexImageNode, urlForImageIdentifier imageIdentifier: ASImageIdentifier) -> URL? { + guard let imageNodeIndex = mediaMultiplexImageNodes.firstIndex(of: imageNode) else { return nil } + guard imageNodeIndex < mosaicImageViewModel.metas.count else { return nil } + let meta = mosaicImageViewModel.metas[imageNodeIndex] + switch imageIdentifier { + case "url" as NSString: + return meta.url + case "previewURL" as NSString: + return meta.priviewURL + default: + assertionFailure() + return nil + } + } +} diff --git a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift index 888d4dffe..265ce245b 100644 --- a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift @@ -16,16 +16,14 @@ struct MosaicImageViewModel { init(mediaAttachments: [Attachment]) { var metas: [MosaicMeta] = [] for element in mediaAttachments where element.type == .image { - // Display original on the iPad/Mac - guard let previewURL = element.previewURL else { continue } - let urlString = UIDevice.current.userInterfaceIdiom == .phone ? previewURL : element.url guard let meta = element.meta, let width = meta.original?.width, let height = meta.original?.height, - let url = URL(string: urlString) else { + let url = URL(string: element.url) else { continue } let mosaicMeta = MosaicMeta( + priviewURL: element.previewURL.flatMap { URL(string: $0) }, url: url, size: CGSize(width: width, height: height), blurhash: element.blurhash, @@ -40,7 +38,8 @@ struct MosaicImageViewModel { struct MosaicMeta { static let edgeMaxLength: CGFloat = 20 - + + let priviewURL: URL? let url: URL let size: CGSize let blurhash: String?