Refactoring [skip ci]

This commit is contained in:
Ali 2023-04-19 23:47:38 +04:00
parent 61b95461d5
commit 6a548e11a6
277 changed files with 613 additions and 19657 deletions

View File

@ -20,7 +20,6 @@ swift_library(
"//submodules/Postbox:Postbox", "//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore", "//submodules/TelegramCore:TelegramCore",
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources", "//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
"//submodules/MeshAnimationCache:MeshAnimationCache",
"//submodules/Utils/RangeSet:RangeSet", "//submodules/Utils/RangeSet:RangeSet",
"//submodules/InAppPurchaseManager:InAppPurchaseManager", "//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TextFormat:TextFormat", "//submodules/TextFormat:TextFormat",

View File

@ -10,7 +10,6 @@ import AsyncDisplayKit
import Display import Display
import DeviceLocationManager import DeviceLocationManager
import TemporaryCachedPeerDataManager import TemporaryCachedPeerDataManager
import MeshAnimationCache
import InAppPurchaseManager import InAppPurchaseManager
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
@ -935,7 +934,6 @@ public protocol AccountContext: AnyObject {
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get } var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
var cachedGroupCallContexts: AccountGroupCallContextCache { get } var cachedGroupCallContexts: AccountGroupCallContextCache { get }
var meshAnimationCache: MeshAnimationCache { get }
var animationCache: AnimationCache { get } var animationCache: AnimationCache { get }
var animationRenderer: MultiAnimationRenderer { get } var animationRenderer: MultiAnimationRenderer { get }

View File

@ -95,8 +95,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
} }
} }
public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? { public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManagerPlayerType? {
func extractFileMedia(_ message: Message) -> TelegramMediaFile? { func extractFileMedia(_ message: EngineMessage) -> TelegramMediaFile? {
var file: TelegramMediaFile? var file: TelegramMediaFile?
for media in message.media { for media in message.media {
if let media = media as? TelegramMediaFile { if let media = media as? TelegramMediaFile {
@ -120,7 +120,7 @@ public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayer
return nil return nil
} }
public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? { public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
if isGlobalSearch && !isDownloadList { if isGlobalSearch && !isDownloadList {
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index)) return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
} else if isRecentActions && !isDownloadList { } else if isRecentActions && !isDownloadList {

View File

@ -3,7 +3,6 @@ import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import AccountContext import AccountContext
import TelegramPresentationData import TelegramPresentationData
@ -371,7 +370,7 @@ private final class DayComponent: Component {
private var currentSelection: DaySelection? private var currentSelection: DaySelection?
private(set) var timestamp: Int32? private(set) var timestamp: Int32?
private(set) var index: MessageIndex? private(set) var index: EngineMessage.Index?
private var isHighlightingEnabled: Bool = false private var isHighlightingEnabled: Bool = false
init() { init() {
@ -983,12 +982,12 @@ public final class CalendarMessageScreen: ViewController {
private weak var controller: CalendarMessageScreen? private weak var controller: CalendarMessageScreen?
private let context: AccountContext private let context: AccountContext
private let peerId: PeerId private let peerId: EnginePeer.Id
private let initialTimestamp: Int32 private let initialTimestamp: Int32
private let enableMessageRangeDeletion: Bool private let enableMessageRangeDeletion: Bool
private let canNavigateToEmptyDays: Bool private let canNavigateToEmptyDays: Bool
private let navigateToOffset: (Int, Int32) -> Void private let navigateToOffset: (Int, Int32) -> Void
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
private var presentationData: PresentationData private var presentationData: PresentationData
private var scrollView: Scroller private var scrollView: Scroller
@ -1019,13 +1018,13 @@ public final class CalendarMessageScreen: ViewController {
init( init(
controller: CalendarMessageScreen, controller: CalendarMessageScreen,
context: AccountContext, context: AccountContext,
peerId: PeerId, peerId: EnginePeer.Id,
calendarSource: SparseMessageCalendar, calendarSource: SparseMessageCalendar,
initialTimestamp: Int32, initialTimestamp: Int32,
enableMessageRangeDeletion: Bool, enableMessageRangeDeletion: Bool,
canNavigateToEmptyDays: Bool, canNavigateToEmptyDays: Bool,
navigateToOffset: @escaping (Int, Int32) -> Void, navigateToOffset: @escaping (Int, Int32) -> Void,
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
) { ) {
self.controller = controller self.controller = controller
self.context = context self.context = context
@ -1783,9 +1782,9 @@ public final class CalendarMessageScreen: ViewController {
guard let calendarState = self.calendarState else { guard let calendarState = self.calendarState else {
return return
} }
var messageMap: [Message] = [] var messageMap: [EngineMessage] = []
for (_, entry) in calendarState.messagesByDay { for (_, entry) in calendarState.messagesByDay {
messageMap.append(entry.message) messageMap.append(EngineMessage(entry.message))
} }
var updatedMedia: [Int: [Int: DayMedia]] = [:] var updatedMedia: [Int: [Int: DayMedia]] = [:]
@ -1805,7 +1804,7 @@ public final class CalendarMessageScreen: ViewController {
mediaLoop: for media in message.media { mediaLoop: for media in message.media {
switch media { switch media {
case _ as TelegramMediaImage, _ as TelegramMediaFile: case _ as TelegramMediaImage, _ as TelegramMediaFile:
updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media)) updatedMedia[i]![day] = DayMedia(message: message, media: EngineMedia(media))
break mediaLoop break mediaLoop
default: default:
break break
@ -1830,13 +1829,13 @@ public final class CalendarMessageScreen: ViewController {
} }
private let context: AccountContext private let context: AccountContext
private let peerId: PeerId private let peerId: EnginePeer.Id
private let calendarSource: SparseMessageCalendar private let calendarSource: SparseMessageCalendar
private let initialTimestamp: Int32 private let initialTimestamp: Int32
private let enableMessageRangeDeletion: Bool private let enableMessageRangeDeletion: Bool
private let canNavigateToEmptyDays: Bool private let canNavigateToEmptyDays: Bool
private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
private var presentationData: PresentationData private var presentationData: PresentationData
@ -1844,13 +1843,13 @@ public final class CalendarMessageScreen: ViewController {
public init( public init(
context: AccountContext, context: AccountContext,
peerId: PeerId, peerId: EnginePeer.Id,
calendarSource: SparseMessageCalendar, calendarSource: SparseMessageCalendar,
initialTimestamp: Int32, initialTimestamp: Int32,
enableMessageRangeDeletion: Bool, enableMessageRangeDeletion: Bool,
canNavigateToEmptyDays: Bool, canNavigateToEmptyDays: Bool,
navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void, navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void,
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
) { ) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId

View File

@ -3,7 +3,6 @@ import AsyncDisplayKit
import Display import Display
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext
import PresentationDataUtils import PresentationDataUtils
@ -17,6 +16,26 @@ import ConfettiEffect
import TelegramUniversalVideoContent import TelegramUniversalVideoContent
import SolidRoundedButtonNode import SolidRoundedButtonNode
private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? {
if useTotalFileAllocatedSize {
let url = URL(fileURLWithPath: path)
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) {
if values.isRegularFile ?? false {
if let fileSize = values.totalFileAllocatedSize {
return Int64(fileSize)
}
}
}
}
var value = stat()
if stat(path, &value) == 0 {
return value.st_size
} else {
return nil
}
}
private final class ProgressEstimator { private final class ProgressEstimator {
private var averageProgressPerSecond: Double = 0.0 private var averageProgressPerSecond: Double = 0.0
private var lastMeasurement: (Double, Float)? private var lastMeasurement: (Double, Float)?
@ -91,7 +110,7 @@ private final class ImportManager {
return self.statePromise.get() return self.statePromise.get()
} }
init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) { init(account: Account, peerId: EnginePeer.Id, mainFile: EngineTempBox.File, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
self.account = account self.account = account
self.archivePath = archivePath self.archivePath = archivePath
self.entries = entries self.entries = entries
@ -234,8 +253,8 @@ private final class ImportManager {
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)") Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in let unpackedFile = Signal<EngineTempBox.File, ImportError> { subscriber in
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path) let tempFile = EngineTempBox.shared.tempFile(fileName: entry.0.path)
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...") Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
let startTime = CACurrentMediaTime() let startTime = CACurrentMediaTime()
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) { if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
@ -440,9 +459,9 @@ public final class ChatImportActivityScreen: ViewController {
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) { if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black) let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])]) let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil) let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0)) videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
@ -724,9 +743,9 @@ public final class ChatImportActivityScreen: ViewController {
private let context: AccountContext private let context: AccountContext
private var presentationData: PresentationData private var presentationData: PresentationData
fileprivate let cancel: () -> Void fileprivate let cancel: () -> Void
fileprivate var peerId: PeerId fileprivate var peerId: EnginePeer.Id
private let archivePath: String? private let archivePath: String?
private let mainEntry: TempBoxFile private let mainEntry: EngineTempBox.File
private let totalBytes: Int64 private let totalBytes: Int64
private let totalMediaBytes: Int64 private let totalMediaBytes: Int64
private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)] private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]
@ -746,7 +765,7 @@ public final class ChatImportActivityScreen: ViewController {
} }
} }
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) { public init(context: AccountContext, cancel: @escaping () -> Void, peerId: EnginePeer.Id, archivePath: String?, mainEntry: EngineTempBox.File, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
self.context = context self.context = context
self.cancel = cancel self.cancel = cancel
self.peerId = peerId self.peerId = peerId
@ -818,7 +837,7 @@ public final class ChatImportActivityScreen: ViewController {
self.progressEstimator = ProgressEstimator() self.progressEstimator = ProgressEstimator()
self.beganCompletion = false self.beganCompletion = false
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError> let resolvedPeerId: Signal<EnginePeer.Id, ImportManager.ImportError>
if self.peerId.namespace == Namespaces.Peer.CloudGroup { if self.peerId.namespace == Namespaces.Peer.CloudGroup {
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId) resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|> mapError { _ -> ImportManager.ImportError in |> mapError { _ -> ImportManager.ImportError in

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import PresentationDataUtils import PresentationDataUtils
@ -30,8 +29,8 @@ private final class ChatListFilterPresetControllerArguments {
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
let openAddIncludePeer: () -> Void let openAddIncludePeer: () -> Void
let openAddExcludePeer: () -> Void let openAddExcludePeer: () -> Void
let deleteIncludePeer: (PeerId) -> Void let deleteIncludePeer: (EnginePeer.Id) -> Void
let deleteExcludePeer: (PeerId) -> Void let deleteExcludePeer: (EnginePeer.Id) -> Void
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
@ -49,8 +48,8 @@ private final class ChatListFilterPresetControllerArguments {
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void, updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
openAddIncludePeer: @escaping () -> Void, openAddIncludePeer: @escaping () -> Void,
openAddExcludePeer: @escaping () -> Void, openAddExcludePeer: @escaping () -> Void,
deleteIncludePeer: @escaping (PeerId) -> Void, deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
deleteExcludePeer: @escaping (PeerId) -> Void, deleteExcludePeer: @escaping (EnginePeer.Id) -> Void,
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void, setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void, deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void, deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
@ -93,7 +92,7 @@ private enum ChatListFilterPresetControllerSection: Int32 {
private enum ChatListFilterPresetEntryStableId: Hashable { private enum ChatListFilterPresetEntryStableId: Hashable {
case index(Int) case index(Int)
case peer(PeerId) case peer(EnginePeer.Id)
case includePeerInfo case includePeerInfo
case excludePeerInfo case excludePeerInfo
case includeCategory(ChatListFilterIncludeCategory) case includeCategory(ChatListFilterIncludeCategory)
@ -311,7 +310,7 @@ private extension ChatListFilterCategoryIcon {
} }
private enum ChatListFilterRevealedItemId: Equatable { private enum ChatListFilterRevealedItemId: Equatable {
case peer(PeerId) case peer(EnginePeer.Id)
case includeCategory(ChatListFilterIncludeCategory) case includeCategory(ChatListFilterIncludeCategory)
case excludeCategory(ChatListFilterExcludeCategory) case excludeCategory(ChatListFilterExcludeCategory)
} }
@ -573,8 +572,8 @@ private struct ChatListFilterPresetControllerState: Equatable {
var excludeMuted: Bool var excludeMuted: Bool
var excludeRead: Bool var excludeRead: Bool
var excludeArchived: Bool var excludeArchived: Bool
var additionallyIncludePeers: [PeerId] var additionallyIncludePeers: [EnginePeer.Id]
var additionallyExcludePeers: [PeerId] var additionallyExcludePeers: [EnginePeer.Id]
var revealedItemId: ChatListFilterRevealedItemId? var revealedItemId: ChatListFilterRevealedItemId?
var expandedSections: Set<FilterSection> var expandedSections: Set<FilterSection>
@ -825,7 +824,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
return return
} }
var includePeers: [PeerId] = [] var includePeers: [EnginePeer.Id] = []
for peerId in peerIds { for peerId in peerIds {
switch peerId { switch peerId {
case let .peer(id): case let .peer(id):
@ -838,7 +837,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks { if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks {
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) }) let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
var removedPeers: [PeerId] = [] var removedPeers: [EnginePeer.Id] = []
if let data = filter.data { if let data = filter.data {
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) }) removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
} }
@ -951,7 +950,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
return return
} }
var excludePeers: [PeerId] = [] var excludePeers: [EnginePeer.Id] = []
for peerId in peerIds { for peerId in peerIds {
switch peerId { switch peerId {
case let .peer(id): case let .peer(id):
@ -1144,7 +1143,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id))) sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id)))
} }
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:]) let currentPeers = Atomic<[EnginePeer.Id: EngineRenderedPeer]>(value: [:])
let stateWithPeers = statePromise.get() let stateWithPeers = statePromise.get()
|> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in |> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in
let currentPeersValue = currentPeers.with { $0 } let currentPeersValue = currentPeers.with { $0 }

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
@ -39,7 +38,7 @@ private enum ChatListFilterPresetListSection: Int32 {
case list case list
} }
private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String { private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
if peers.isEmpty { if peers.isEmpty {
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
} else { } else {

View File

@ -3,7 +3,6 @@ import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import ItemListUI import ItemListUI

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData

View File

@ -665,13 +665,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openPasswordSetup() nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount: case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro() nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
} }
case .hide: case .hide:
switch notice { switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default: default:
break break
} }
@ -966,13 +962,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openPasswordSetup() nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount: case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro() nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
} }
case .hide: case .hide:
switch notice { switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default: default:
break break
} }

View File

@ -210,8 +210,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize) strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
switch item.notice { switch item.notice {
case .chatFolderUpdates:
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.ChatList_HideAction, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
default: default:
strongSelf.setRevealOptions((left: [], right: [])) strongSelf.setRevealOptions((left: [], right: []))
} }

View File

@ -1,12 +1,11 @@
import Foundation import Foundation
import UIKit import UIKit
import TelegramCore import TelegramCore
import Postbox
import SwiftSignalKit import SwiftSignalKit
import UniversalMediaPlayer import UniversalMediaPlayer
import AccountContext import AccountContext
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus?, NoError> { private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus?, NoError> {
guard let playerType = peerMessageMediaPlayerType(message) else { guard let playerType = peerMessageMediaPlayerType(message) else {
return .single(nil) return .single(nil)
} }
@ -21,7 +20,7 @@ private func internalMessageFileMediaPlaybackStatus(context: AccountContext, fil
} }
} }
public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus, NoError> { public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus, NoError> {
var duration = 0.0 var duration = 0.0
if let value = file.duration { if let value = file.duration {
duration = Double(value) duration = Double(value)
@ -33,7 +32,7 @@ public func messageFileMediaPlaybackStatus(context: AccountContext, file: Telegr
} }
} }
public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<Float, NoError> { public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<Float, NoError> {
guard let playerType = peerMessageMediaPlayerType(message) else { guard let playerType = peerMessageMediaPlayerType(message) else {
return .never() return .never()
} }
@ -45,7 +44,7 @@ public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, fi
} }
} }
public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal<FileMediaResourceStatus, NoError> { public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) |> map { status -> MediaPlayerPlaybackStatus? in let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) |> map { status -> MediaPlayerPlaybackStatus? in
return status?.status return status?.status
} }
@ -99,7 +98,7 @@ public func messageFileMediaResourceStatus(context: AccountContext, file: Telegr
} }
} }
public func messageImageMediaResourceStatus(context: AccountContext, image: TelegramMediaImage, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal<FileMediaResourceStatus, NoError> { public func messageImageMediaResourceStatus(context: AccountContext, image: TelegramMediaImage, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
if message.flags.isSending { if message.flags.isSending {
return combineLatest(messageMediaImageStatus(context: context, messageId: message.id, image: image), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 }) return combineLatest(messageMediaImageStatus(context: context, messageId: message.id, image: image), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in |> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in

View File

@ -37,13 +37,13 @@ private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, med
switch block { switch block {
case let .image(id, caption, _, _): case let .image(id, caption, _, _):
if let m = media[id] { if let m = media[id] {
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))] let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: EngineMedia(m), url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
counter += 1 counter += 1
return result return result
} }
case let .video(id, caption, _, _): case let .video(id, caption, _, _):
if let m = media[id] { if let m = media[id] {
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))] let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: EngineMedia(m), url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
counter += 1 counter += 1
return result return result
} }
@ -82,7 +82,7 @@ public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galle
} }
if !found { if !found {
result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: galleryMedia, url: nil, caption: nil, credit: nil), caption: nil, credit: nil, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0) result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: EngineMedia(galleryMedia), url: nil, caption: nil, credit: nil), caption: nil, credit: nil, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0)
} }
for i in 0 ..< result.count { for i in 0 ..< result.count {
@ -123,7 +123,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
if case .suggestedProfilePhoto = action.action { if case .suggestedProfilePhoto = action.action {
isSuggested = true isSuggested = true
} }
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false, nil)]) let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false, nil)])
let sourceCorners: AvatarGalleryController.SourceCorners let sourceCorners: AvatarGalleryController.SourceCorners
if case .photoUpdated = action.action { if case .photoUpdated = action.action {
@ -131,7 +131,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
} else { } else {
sourceCorners = .round sourceCorners = .round
} }
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in let galleryController = AvatarGalleryController(context: context, peer: peer, sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in
}) })
return .chatAvatars(galleryController, image) return .chatAvatars(galleryController, image)

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import TelegramCore import TelegramCore
import Postbox
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import TelegramBaseController import TelegramBaseController

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Vision import Vision
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
@ -27,8 +26,8 @@ private final class CachedImageRecognizedContent: Codable {
} }
} }
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId) -> Signal<CachedImageRecognizedContent?, NoError> { private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal<CachedImageRecognizedContent?, NoError> {
let key = ValueBoxKey(length: 20) let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace) key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value()) key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value()) key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
@ -40,8 +39,8 @@ private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: Mes
} }
} }
private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> { private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> {
let key = ValueBoxKey(length: 20) let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace) key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value()) key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value()) key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
@ -333,7 +332,7 @@ private func recognizeContent(in image: UIImage?) -> Signal<[RecognizedContent],
} }
} }
public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: MessageId) -> Signal<[RecognizedContent], NoError> { public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: EngineMessage.Id) -> Signal<[RecognizedContent], NoError> {
if context.sharedContext.immediateExperimentalUISettings.disableImageContentAnalysis { if context.sharedContext.immediateExperimentalUISettings.disableImageContentAnalysis {
return .single([]) return .single([])
} }

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
enum StickerVerificationStatus { enum StickerVerificationStatus {
@ -74,7 +73,7 @@ public class ImportStickerPack {
let emojis: [String] let emojis: [String]
let keywords: String let keywords: String
let uuid: UUID let uuid: UUID
var resource: MediaResource? var resource: EngineMediaResource?
init(content: Content, emojis: [String], keywords: String, uuid: UUID = UUID()) { init(content: Content, emojis: [String], keywords: String, uuid: UUID = UUID()) {
self.content = content self.content = content

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramUIPreferences import TelegramUIPreferences
@ -87,23 +86,23 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
return return
} }
var signals: [Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError>] = [] var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = []
for sticker in strongSelf.stickerPack.stickers { for sticker in strongSelf.stickerPack.stickers {
if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] { if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] {
signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType) signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource._asResource(), alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType)
|> map { result -> (UUID, StickerVerificationStatus, MediaResource?) in |> map { result -> (UUID, StickerVerificationStatus, EngineMediaResource?) in
switch result { switch result {
case .progress: case .progress:
return (sticker.uuid, .loading, nil) return (sticker.uuid, .loading, nil)
case let .complete(resource, mimeType): case let .complete(resource, mimeType):
if ["application/x-tgsticker", "video/webm"].contains(mimeType) { if ["application/x-tgsticker", "video/webm"].contains(mimeType) {
return (sticker.uuid, .verified, resource) return (sticker.uuid, .verified, EngineMediaResource(resource))
} else { } else {
return (sticker.uuid, .declined, nil) return (sticker.uuid, .declined, nil)
} }
} }
} }
|> `catch` { _ -> Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError> in |> `catch` { _ -> Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError> in
return .single((sticker.uuid, .declined, nil)) return .single((sticker.uuid, .declined, nil))
}) })
} }
@ -115,7 +114,7 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
} }
var verifiedStickers = Set<UUID>() var verifiedStickers = Set<UUID>()
var declinedStickers = Set<UUID>() var declinedStickers = Set<UUID>()
var uploadedStickerResources: [UUID: MediaResource] = [:] var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
for (uuid, result, resource) in results { for (uuid, result, resource) in results {
switch result { switch result {
case .verified: case .verified:

View File

@ -3,7 +3,6 @@ import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
@ -52,8 +51,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
private let context: AccountContext private let context: AccountContext
private var presentationData: PresentationData private var presentationData: PresentationData
private var stickerPack: ImportStickerPack? private var stickerPack: ImportStickerPack?
var stickerResources: [UUID: MediaResource] = [:] var stickerResources: [UUID: EngineMediaResource] = [:]
private var uploadedStickerResources: [UUID: MediaResource] = [:] private var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
private var stickerPackReady = true private var stickerPackReady = true
private var containerLayout: (ContainerViewLayout, CGFloat)? private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -623,11 +622,11 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
} }
if let resource = self.uploadedStickerResources[item.stickerItem.uuid] { if let resource = self.uploadedStickerResources[item.stickerItem.uuid] {
if let localResource = item.stickerItem.resource { if let localResource = item.stickerItem.resource {
self.context.account.postbox.mediaBox.copyResourceData(from: localResource.id, to: resource.id) self.context.account.postbox.mediaBox.copyResourceData(from: localResource._asResource().id, to: resource._asResource().id)
} }
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
} else if let resource = item.stickerItem.resource { } else if let resource = item.stickerItem.resource {
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
} }
} }
var thumbnailSticker: ImportSticker? var thumbnailSticker: ImportSticker?
@ -695,23 +694,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
let context = strongSelf.context let context = strongSelf.context
Queue.mainQueue().after(1.0) { Queue.mainQueue().after(1.0) {
var firstItem: StickerPackItem? let firstItem: StickerPackItem? = firstStickerItem?.stickerPackItem
if let firstStickerItem = firstStickerItem, let resource = firstStickerItem.resource as? TelegramMediaResource {
var fileAttributes: [TelegramMediaFileAttribute] = []
if firstStickerItem.mimeType == "video/webm" {
fileAttributes.append(.FileName(fileName: "sticker.webm"))
fileAttributes.append(.Animated)
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
} else if firstStickerItem.mimeType == "application/x-tgsticker" {
fileAttributes.append(.FileName(fileName: "sticker.tgs"))
fileAttributes.append(.Animated)
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
} else {
fileAttributes.append(.FileName(fileName: "sticker.webp"))
}
fileAttributes.append(.ImageSize(size: firstStickerItem.dimensions))
firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: firstStickerItem.mimeType, size: nil, attributes: fileAttributes), indexKeys: [])
}
strongSelf.presentInGlobalOverlay?(UndoOverlayController(presentationData: strongSelf.presentationData, content: .stickersModified(title: strongSelf.presentationData.strings.StickerPackActionInfo_AddedTitle, text: strongSelf.presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: firstItem ?? items.first, context: strongSelf.context), elevatedLayout: false, action: { action in strongSelf.presentInGlobalOverlay?(UndoOverlayController(presentationData: strongSelf.presentationData, content: .stickersModified(title: strongSelf.presentationData.strings.StickerPackActionInfo_AddedTitle, text: strongSelf.presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: firstItem ?? items.first, context: strongSelf.context), elevatedLayout: false, action: { action in
if case .info = action { if case .info = action {
(navigationController?.viewControllers.last as? ViewController)?.present(StickerPackScreen(context: context, mode: .settings, mainStickerPack: .id(id: info.id.id, accessHash: info.accessHash), stickerPacks: [], parentNavigationController: navigationController, actionPerformed: { _ in (navigationController?.viewControllers.last as? ViewController)?.present(StickerPackScreen(context: context, mode: .settings, mainStickerPack: .id(id: info.id.id, accessHash: info.accessHash), stickerPacks: [], parentNavigationController: navigationController, actionPerformed: { _ in
@ -800,7 +783,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
}) })
} }
func updateStickerPack(_ stickerPack: ImportStickerPack, verifiedStickers: Set<UUID>, declinedStickers: Set<UUID>, uploadedStickerResources: [UUID: MediaResource]) { func updateStickerPack(_ stickerPack: ImportStickerPack, verifiedStickers: Set<UUID>, declinedStickers: Set<UUID>, uploadedStickerResources: [UUID: EngineMediaResource]) {
self.stickerPack = stickerPack self.stickerPack = stickerPack
self.uploadedStickerResources = uploadedStickerResources self.uploadedStickerResources = uploadedStickerResources
var updatedItems: [StickerPackPreviewGridEntry] = [] var updatedItems: [StickerPackPreviewGridEntry] = []
@ -813,8 +796,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
} else { } else {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data)
item.resource = resource item.resource = EngineMediaResource(resource)
self.stickerResources[item.uuid] = resource self.stickerResources[item.uuid] = EngineMediaResource(resource)
} }
var isInitiallyVerified = false var isInitiallyVerified = false
if case .image = item.content { if case .image = item.content {

View File

@ -3,7 +3,6 @@ import UIKit
import SwiftSignalKit import SwiftSignalKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import AccountContext import AccountContext

View File

@ -4,7 +4,6 @@ import Display
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import AsyncDisplayKit import AsyncDisplayKit
import Postbox
import StickerResources import StickerResources
import AccountContext import AccountContext
import AnimatedStickerNode import AnimatedStickerNode
@ -142,7 +141,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
if case .video = stickerItem.content { if case .video = stickerItem.content {
isVideo = true isVideo = true
} }
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource._asResource(), isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
} }
animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
} else { } else {

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import StickerResources import StickerResources
@ -87,7 +86,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
if case .video = item.content { if case .video = item.content {
isVideo = true isVideo = true
} }
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource._asResource(), isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
} }
self.animationNode?.visibility = true self.animationNode?.visibility = true
} }

View File

@ -1,8 +1,8 @@
import Foundation import Foundation
import UIKit import UIKit
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import Postbox
import TelegramUIPreferences import TelegramUIPreferences
import PersistentStringHash import PersistentStringHash

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import AccountContext import AccountContext
import UrlHandling import UrlHandling

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData
@ -20,11 +19,11 @@ public final class InstantPageArticleItem: InstantPageItem {
let contentSize: CGSize let contentSize: CGSize
let cover: TelegramMediaImage? let cover: TelegramMediaImage?
let url: String let url: String
let webpageId: MediaId let webpageId: EngineMedia.Id
let rtl: Bool let rtl: Bool
let hasRTL: Bool let hasRTL: Bool
init(frame: CGRect, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool, hasRTL: Bool) { init(frame: CGRect, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, rtl: Bool, hasRTL: Bool) {
self.frame = frame self.frame = frame
self.userLocation = userLocation self.userLocation = userLocation
self.webPage = webPage self.webPage = webPage
@ -73,7 +72,7 @@ public final class InstantPageArticleItem: InstantPageItem {
} }
} }
func layoutArticleItem(theme: InstantPageTheme, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: MediaId, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem { func layoutArticleItem(theme: InstantPageTheme, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem {
let inset: CGFloat = 17.0 let inset: CGFloat = 17.0
let imageSpacing: CGFloat = 10.0 let imageSpacing: CGFloat = 10.0
var sideInset = inset var sideInset = inset

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
@ -20,14 +19,14 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
private var imageNode: TransformImageNode? private var imageNode: TransformImageNode?
let url: String let url: String
let webpageId: MediaId let webpageId: EngineMedia.Id
let cover: TelegramMediaImage? let cover: TelegramMediaImage?
private let openUrl: (InstantPageUrlItem) -> Void private let openUrl: (InstantPageUrlItem) -> Void
private var fetchedDisposable = MetaDisposable() private var fetchedDisposable = MetaDisposable()
init(context: AccountContext, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, openUrl: @escaping (InstantPageUrlItem) -> Void) { init(context: AccountContext, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, openUrl: @escaping (InstantPageUrlItem) -> Void) {
self.item = item self.item = item
self.url = url self.url = url
self.webpageId = webpageId self.webpageId = webpageId

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -1,7 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import TelegramCore import TelegramCore
import Postbox
import SwiftSignalKit import SwiftSignalKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
@ -36,7 +35,7 @@ private func generatePauseButton(color: UIColor) -> UIImage? {
private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString { private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString {
let string = NSMutableAttributedString() let string = NSMutableAttributedString()
if let file = media.media as? TelegramMediaFile { if case let .file(file) = media.media {
loop: for attribute in file.attributes { loop: for attribute in file.attributes {
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice { if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
let titleText: String = title ?? strings.MediaPlayer_UnknownTrack let titleText: String = title ?? strings.MediaPlayer_UnknownTrack
@ -101,7 +100,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5), chapters: [])) self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5), chapters: []))
let playlistType: MediaManagerPlayerType let playlistType: MediaManagerPlayerType
if let file = self.media.media as? TelegramMediaFile { if case let .file(file) = self.media.media {
playlistType = file.isVoice ? .voice : .music playlistType = file.isVoice ? .voice : .music
} else { } else {
playlistType = .music playlistType = .music

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -555,7 +555,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
if item is InstantPageWebEmbedItem { if item is InstantPageWebEmbedItem {
embedIndex += 1 embedIndex += 1
} }
if let imageItem = item as? InstantPageImageItem, imageItem.media.media is TelegramMediaWebpage { if let imageItem = item as? InstantPageImageItem, case .webpage = imageItem.media.media {
embedIndex += 1 embedIndex += 1
} }
if item is InstantPageDetailsItem { if item is InstantPageDetailsItem {
@ -1003,17 +1003,17 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
private func longPressMedia(_ media: InstantPageMedia) { private func longPressMedia(_ media: InstantPageMedia) {
let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
if let strongSelf = self, let image = media.media as? TelegramMediaImage { if let strongSelf = self, case let .image(image) = media.media {
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
} }
}), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in }), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in
if let strongSelf = self, let image = media.media as? TelegramMediaImage { if let strongSelf = self, case let .image(image) = media.media {
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
} }
}), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage { if let strongSelf = self, let webPage = strongSelf.webPage, case let .image(image) = media.media {
strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil)
} }
})], catchTapsOutside: true) })], catchTapsOutside: true)
@ -1406,7 +1406,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
return return
} }
if let map = media.media as? TelegramMediaMap { if case let .geo(map) = media.media {
let controllerParams = LocationViewParams(sendLiveLocation: { _ in let controllerParams = LocationViewParams(sendLiveLocation: { _ in
}, stopLiveLocation: { _ in }, stopLiveLocation: { _ in
}, openUrl: { _ in }, openPeer: { _ in }, openUrl: { _ in }, openPeer: { _ in
@ -1420,12 +1420,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
return return
} }
if let file = media.media as? TelegramMediaFile, (file.isVoice || file.isMusic) { if case let .file(file) = media.media, (file.isVoice || file.isMusic) {
var medias: [InstantPageMedia] = [] var medias: [InstantPageMedia] = []
var initialIndex = 0 var initialIndex = 0
for item in items { for item in items {
for itemMedia in item.medias { for itemMedia in item.medias {
if let itemFile = itemMedia.media as? TelegramMediaFile, (itemFile.isVoice || itemFile.isMusic) { if case let .file(itemFile) = itemMedia.media, (itemFile.isVoice || itemFile.isMusic) {
if itemMedia.index == media.index { if itemMedia.index == media.index {
initialIndex = medias.count initialIndex = medias.count
} }
@ -1440,16 +1440,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
var fromPlayingVideo = false var fromPlayingVideo = false
var entries: [InstantPageGalleryEntry] = [] var entries: [InstantPageGalleryEntry] = []
if media.media is TelegramMediaWebpage { if case let .webpage(webPage) = media.media {
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil)) entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
} else if let file = media.media as? TelegramMediaFile, file.isAnimated { } else if case let .file(file) = media.media, file.isAnimated {
fromPlayingVideo = true fromPlayingVideo = true
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil)) entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
} else { } else {
fromPlayingVideo = true fromPlayingVideo = true
var medias: [InstantPageMedia] = mediasFromItems(items) var medias: [InstantPageMedia] = mediasFromItems(items)
medias = medias.filter { medias = medias.filter { item in
return $0.media is TelegramMediaImage || $0.media is TelegramMediaFile switch item.media {
case .image, .file:
return true
default:
return false
}
} }
for media in medias { for media in medias {

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -96,9 +96,9 @@ public struct InstantPageGalleryEntry: Equatable {
} }
} }
if let image = self.media.media as? TelegramMediaImage { if case let .image(image) = self.media.media {
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
} else if let file = self.media.media as? TelegramMediaFile { } else if case let .file(file) = self.media.media {
if file.isVideo { if file.isVideo {
var indexData: GalleryItemIndexData? var indexData: GalleryItemIndexData?
if let location = self.location { if let location = self.location {
@ -122,7 +122,7 @@ public struct InstantPageGalleryEntry: Equatable {
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions) return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
} }
} else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content { } else if case let .webpage(embedWebpage) = self.media.media, case let .Loaded(webpageContent) = embedWebpage.content {
if webpageContent.url.hasSuffix(".m3u8") { if webpageContent.url.hasSuffix(".m3u8") {
let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), userLocation: userLocation, content: .url(webpageContent.url), streamVideo: true, loopVideo: false) let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), userLocation: userLocation, content: .url(webpageContent.url), streamVideo: true, loopVideo: false)
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, { makeArguments, navigationController, present in return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, { makeArguments, navigationController, present in

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
@ -43,7 +42,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private var currentSize: CGSize? private var currentSize: CGSize?
private var fetchStatus: MediaResourceStatus? private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchedDisposable = MetaDisposable() private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable() private var statusDisposable = MetaDisposable()
@ -72,7 +71,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.pinchContainerNode.contentNode.addSubnode(self.imageNode) self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
self.addSubnode(self.pinchContainerNode) self.addSubnode(self.pinchContainerNode)
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference)) self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
@ -92,7 +91,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch { displayLinkDispatcher.dispatch {
if let strongSelf = self { if let strongSelf = self {
strongSelf.fetchStatus = status strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus() strongSelf.updateFetchStatus()
} }
} }
@ -105,7 +104,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.pinchContainerNode.contentNode.addSubnode(self.statusNode) self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
} }
} else if let file = media.media as? TelegramMediaFile { } else if case let .file(file) = media.media {
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") { if file.mimeType.hasPrefix("image/") {
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) { if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
@ -119,7 +118,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.statusNode.transitionToState(.play(.white), animated: false, completion: {}) self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
self.pinchContainerNode.contentNode.addSubnode(self.statusNode) self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
} }
} else if let map = media.media as? TelegramMediaMap { } else if case let .geo(map) = media.media {
self.addSubnode(self.pinNode) self.addSubnode(self.pinNode)
var dimensions = CGSize(width: 200.0, height: 100.0) var dimensions = CGSize(width: 200.0, height: 100.0)
@ -131,7 +130,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
} }
let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height)) let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height))
self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource)) self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource))
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image { } else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference)) self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start()) self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
@ -211,7 +210,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
if self.currentSize != size || self.themeUpdated { if self.currentSize != size || self.themeUpdated {
self.currentSize = size self.currentSize = size
self.themeUpdated = false self.themeUpdated = false
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size) self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchContainerNode.update(size: size, transition: .immediate) self.pinchContainerNode.update(size: size, transition: .immediate)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size) self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
@ -219,7 +218,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
let radialStatusSize: CGFloat = 50.0 let radialStatusSize: CGFloat = 50.0
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize) self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions.cgSize.aspectFilled(size) let imageSize = largest.dimensions.cgSize.aspectFilled(size)
let boundingSize = size let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0 let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
@ -228,15 +227,15 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
apply() apply()
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0) self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
} else if let file = self.media.media as? TelegramMediaFile, let dimensions = file.dimensions { } else if case let .file(file) = self.media.media, let dimensions = file.dimensions {
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
let imageSize = dimensions.cgSize.aspectFilled(size) let imageSize = dimensions.cgSize.aspectFilled(size)
let boundingSize = size let boundingSize = size
let makeLayout = self.imageNode.asyncLayout() let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor)) let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor))
apply() apply()
} else if self.media.media is TelegramMediaMap { } else if case .geo = self.media.media {
for attribute in self.attributes { for attribute in self.attributes {
if let mapAttribute = attribute as? InstantPageMapAttribute { if let mapAttribute = attribute as? InstantPageMapAttribute {
let imageSize = mapAttribute.dimensions.aspectFilled(size) let imageSize = mapAttribute.dimensions.aspectFilled(size)
@ -254,7 +253,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
let (pinSize, pinApply) = makePinLayout(self.context, theme, .location(nil)) let (pinSize, pinApply) = makePinLayout(self.context, theme, .location(nil))
self.pinNode.frame = CGRect(origin: CGPoint(x: floor((size.width - pinSize.width) / 2.0), y: floor(size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize) self.pinNode.frame = CGRect(origin: CGPoint(x: floor((size.width - pinSize.width) / 2.0), y: floor(size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize)
pinApply() pinApply()
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) { } else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions.cgSize.aspectFilled(size) let imageSize = largest.dimensions.cgSize.aspectFilled(size)
let boundingSize = size let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0 let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
@ -290,7 +289,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
case .Local: case .Local:
switch gesture { switch gesture {
case .tap: case .tap:
if self.media.media is TelegramMediaImage && self.media.index == -1 { if case .image = self.media.media, self.media.index == -1 {
return return
} }
self.openMedia(self.media) self.openMedia(self.media)
@ -311,7 +310,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
} else { } else {
switch gesture { switch gesture {
case .tap: case .tap:
if self.media.media is TelegramMediaImage && self.media.index == -1 { if case .image = self.media.media, self.media.index == -1 {
return return
} }
self.openMedia(self.media) self.openMedia(self.media)

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -1,7 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import TelegramCore import TelegramCore
import Postbox
import Display import Display
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
@ -48,7 +47,7 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP
} }
} }
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout { public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout {
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
@ -373,7 +372,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
contentSize.height += verticalInset contentSize.height += verticalInset
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
case let .image(id, caption, url, webpageId): case let .image(id, caption, url, webpageId):
if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { if case let .image(image) = media[id], let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions let imageSize = largest.dimensions
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0)) var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
@ -397,7 +396,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId) mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
} }
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: image, url: mediaUrl, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false) let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .image(image), url: mediaUrl, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
items.append(mediaItem) items.append(mediaItem)
contentSize.height += filledSize.height contentSize.height += filledSize.height
@ -413,7 +412,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
} }
case let .video(id, caption, autoplay, _): case let .video(id, caption, autoplay, _):
if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions { if case let .file(file) = media[id], let dimensions = file.dimensions {
let imageSize = dimensions let imageSize = dimensions
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0)) var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
@ -433,11 +432,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
if autoplay { if autoplay {
let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: caption.text, credit: caption.credit), interactive: true) let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: caption.text, credit: caption.credit), interactive: true)
items.append(mediaItem) items.append(mediaItem)
} else { } else {
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false) let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
items.append(mediaItem) items.append(mediaItem)
} }
@ -460,11 +459,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var size = CGSize() var size = CGSize()
switch subItem { switch subItem {
case let .image(id, _, _, _): case let .image(id, _, _, _):
if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { if case let .image(image) = media[id], let largest = largestImageRepresentation(image.representations) {
size = largest.dimensions.cgSize size = largest.dimensions.cgSize
} }
case let .video(id, _, _, _): case let .video(id, _, _, _):
if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions { if case let .file(file) = media[id], let dimensions = file.dimensions {
size = dimensions.cgSize size = dimensions.cgSize
} }
default: default:
@ -502,9 +501,15 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
if !author.isEmpty { if !author.isEmpty {
let avatar: TelegramMediaImage? = avatarId.flatMap { media[$0] as? TelegramMediaImage } let avatar: TelegramMediaImage? = avatarId.flatMap { id -> TelegramMediaImage? in
if case let .image(image) = media[id] {
return image
} else {
return nil
}
}
if let avatar = avatar { if let avatar = avatar {
let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: avatar, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: true, fit: false) let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: .image(avatar), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: true, fit: false)
items.append(avatarItem) items.append(avatarItem)
avatarInset += 62.0 avatarInset += 62.0
@ -572,7 +577,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
for subBlock in subItems { for subBlock in subItems {
switch subBlock { switch subBlock {
case let .image(id, caption, url, webpageId): case let .image(id, caption, url, webpageId):
if let image = media[id] as? TelegramMediaImage, let imageSize = largestImageRepresentation(image.representations)?.dimensions { if case let .image(image) = media[id], let imageSize = largestImageRepresentation(image.representations)?.dimensions {
let mediaIndex = mediaIndexCounter let mediaIndex = mediaIndexCounter
mediaIndexCounter += 1 mediaIndexCounter += 1
@ -583,7 +588,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
if let url = url { if let url = url {
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId) mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
} }
itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, url: mediaUrl, caption: caption.text, credit: caption.credit)) itemMedias.append(InstantPageMedia(index: mediaIndex, media: .image(image), url: mediaUrl, caption: caption.text, credit: caption.credit))
} }
break break
default: default:
@ -626,11 +631,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize: CGSize var contentSize: CGSize
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size) let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
let item: InstantPageItem let item: InstantPageItem
if let url = url, let coverId = coverId, let image = media[coverId] as? TelegramMediaImage { if let url = url, let coverId = coverId, case let .image(image) = media[coverId] {
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil) let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil)
let content = TelegramMediaWebpageContent.Loaded(loadedContent) let content = TelegramMediaWebpageContent.Loaded(loadedContent)
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false) item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)
} else { } else {
item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling) item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling)
@ -665,7 +670,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
} }
if let peer = peer { if let peer = peer {
let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL) let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: .channel(peer), safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL)
items.append(item) items.append(item)
if offset.isZero { if offset.isZero {
contentSize.height += 40.0 contentSize.height += 40.0
@ -679,10 +684,10 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize = CGSize(width: boundingWidth, height: 0.0) var contentSize = CGSize(width: boundingWidth, height: 0.0)
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
if let file = media[audioId] as? TelegramMediaFile { if case let .file(file) = media[audioId] {
let mediaIndex = mediaIndexCounter let mediaIndex = mediaIndexCounter
mediaIndexCounter += 1 mediaIndexCounter += 1
let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: nil, credit: nil), webpage: webpage) let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: nil, credit: nil), webpage: webpage)
contentSize.height += item.frame.height contentSize.height += item.frame.height
items.append(item) items.append(item)
@ -765,7 +770,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
for (i, article) in articles.enumerated() { for (i, article) in articles.enumerated() {
var cover: TelegramMediaImage? var cover: TelegramMediaImage?
if let coverId = article.photoId { if let coverId = article.photoId {
cover = media[coverId] as? TelegramMediaImage if case let .image(image) = media[coverId] {
cover = image
}
} }
var styleStack = InstantPageTextStyleStack() var styleStack = InstantPageTextStyleStack()
@ -820,7 +827,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0)
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: -1, media: map, url: nil, caption: caption.text, credit: caption.credit), attributes: attributes, interactive: true, roundCorners: false, fit: false) let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: -1, media: .geo(map), url: nil, caption: caption.text, credit: caption.credit), attributes: attributes, interactive: true, roundCorners: false, fit: false)
items.append(mediaItem) items.append(mediaItem)
contentSize.height += filledSize.height contentSize.height += filledSize.height
@ -850,12 +857,12 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLoc
var contentSize = CGSize(width: boundingWidth, height: 0.0) var contentSize = CGSize(width: boundingWidth, height: 0.0)
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
var media = instantPage.media var media = instantPage.media.mapValues(EngineMedia.init)
if let image = loadedContent.image, let id = image.id { if let image = loadedContent.image, let id = image.id {
media[id] = image media[id] = .image(image)
} }
if let video = loadedContent.file, let id = video.id { if let video = loadedContent.file, let id = video.id {
media[id] = video media[id] = .file(video)
} }
var mediaIndexCounter: Int = 0 var mediaIndexCounter: Int = 0

View File

@ -1,15 +1,14 @@
import Foundation import Foundation
import Postbox
import TelegramCore import TelegramCore
public struct InstantPageMedia: Equatable { public struct InstantPageMedia: Equatable {
public let index: Int public let index: Int
public let media: Media public let media: EngineMedia
public let url: InstantPageUrlItem? public let url: InstantPageUrlItem?
public let caption: RichText? public let caption: RichText?
public let credit: RichText? public let credit: RichText?
public init(index: Int, media: Media, url: InstantPageUrlItem?, caption: RichText?, credit: RichText?) { public init(index: Int, media: EngineMedia, url: InstantPageUrlItem?, caption: RichText?, credit: RichText?) {
self.index = index self.index = index
self.media = media self.media = media
self.url = url self.url = url
@ -18,6 +17,6 @@ public struct InstantPageMedia: Equatable {
} }
public static func ==(lhs: InstantPageMedia, rhs: InstantPageMedia) -> Bool { public static func ==(lhs: InstantPageMedia, rhs: InstantPageMedia) -> Bool {
return lhs.index == rhs.index && lhs.media.isEqual(to: rhs.media) && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit return lhs.index == rhs.index && lhs.media == rhs.media && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit
} }
} }

View File

@ -1,7 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
@ -22,7 +21,11 @@ struct InstantPageMediaPlaylistItemId: SharedMediaPlaylistItemId {
} }
private func extractFileMedia(_ item: InstantPageMedia) -> TelegramMediaFile? { private func extractFileMedia(_ item: InstantPageMedia) -> TelegramMediaFile? {
return item.media as? TelegramMediaFile if case let .file(file) = item.media {
return file
} else {
return nil
}
} }
final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem { final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
@ -114,7 +117,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
} }
struct InstantPageMediaPlaylistId: SharedMediaPlaylistId { struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
let webpageId: MediaId let webpageId: EngineMedia.Id
func isEqual(to: SharedMediaPlaylistId) -> Bool { func isEqual(to: SharedMediaPlaylistId) -> Bool {
if let to = to as? InstantPageMediaPlaylistId { if let to = to as? InstantPageMediaPlaylistId {
@ -125,7 +128,7 @@ struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
} }
struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation { struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation {
let webpageId: MediaId let webpageId: EngineMedia.Id
func isEqual(to: SharedMediaPlaylistLocation) -> Bool { func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
guard let to = to as? InstantPagePlaylistLocation else { guard let to = to as? InstantPagePlaylistLocation else {

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData
@ -14,12 +13,12 @@ public final class InstantPagePeerReferenceItem: InstantPageItem {
public let separatesTiles: Bool = false public let separatesTiles: Bool = false
public let medias: [InstantPageMedia] = [] public let medias: [InstantPageMedia] = []
let initialPeer: Peer let initialPeer: EnginePeer
let safeInset: CGFloat let safeInset: CGFloat
let transparent: Bool let transparent: Bool
let rtl: Bool let rtl: Bool
init(frame: CGRect, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool) { init(frame: CGRect, initialPeer: EnginePeer, safeInset: CGFloat, transparent: Bool, rtl: Bool) {
self.frame = frame self.frame = frame
self.initialPeer = initialPeer self.initialPeer = initialPeer
self.safeInset = safeInset self.safeInset = safeInset

View File

@ -1,7 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import TelegramCore import TelegramCore
import Postbox
import SwiftSignalKit import SwiftSignalKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
@ -64,13 +63,13 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
private let activityIndicator: ActivityIndicator private let activityIndicator: ActivityIndicator
private let checkNode: ASImageNode private let checkNode: ASImageNode
var peer: Peer? var peer: EnginePeer?
private var peerDisposable: Disposable? private var peerDisposable: Disposable?
private let joinDisposable = MetaDisposable() private let joinDisposable = MetaDisposable()
private var joinState: JoinState = .none private var joinState: JoinState = .none
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (EnginePeer) -> Void) { init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, initialPeer: EnginePeer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (EnginePeer) -> Void) {
self.context = context self.context = context
self.strings = strings self.strings = strings
self.nameDisplayOrder = nameDisplayOrder self.nameDisplayOrder = nameDisplayOrder
@ -147,26 +146,26 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
let account = self.context.account let account = self.context.account
let context = self.context let context = self.context
let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer) let signal: Signal<EnginePeer, NoError> = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer._asPeer())
|> mapToSignal({ peer -> Signal<Peer, NoError> in |> mapToSignal({ peer -> Signal<EnginePeer, NoError> in
if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil { if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil {
return .single(peer) |> then(context.engine.peers.resolvePeerByName(name: username) return .single(.channel(peer)) |> then(context.engine.peers.resolvePeerByName(name: username)
|> mapToSignal({ updatedPeer -> Signal<Peer, NoError> in |> mapToSignal({ updatedPeer -> Signal<EnginePeer, NoError> in
if let updatedPeer = updatedPeer { if let updatedPeer = updatedPeer {
return .single(updatedPeer._asPeer()) return .single(updatedPeer)
} else { } else {
return .single(peer) return .single(.channel(peer))
} }
})) }))
} else { } else {
return .single(peer) return .single(EnginePeer(peer))
} }
}) })
self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self { if let strongSelf = self {
strongSelf.peer = peer strongSelf.peer = peer
if let peer = peer as? TelegramChannel { if case let .channel(peer) = peer {
var joinState = strongSelf.joinState var joinState = strongSelf.joinState
if case .member = peer.participationStatus { if case .member = peer.participationStatus {
switch joinState { switch joinState {
@ -210,7 +209,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
private func applyThemeAndStrings(themeUpdated: Bool) { private func applyThemeAndStrings(themeUpdated: Bool) {
if let peer = self.peer { if let peer = self.peer {
let textColor = self.transparent ? UIColor.white : self.theme.panelPrimaryColor let textColor = self.transparent ? UIColor.white : self.theme.panelPrimaryColor
self.nameNode.attributedText = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: textColor) self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: textColor)
} }
let accentColor = self.transparent ? UIColor.white : self.theme.panelAccentColor let accentColor = self.transparent ? UIColor.white : self.theme.panelAccentColor
self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: accentColor), for: []) self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: accentColor), for: [])
@ -300,7 +299,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
@objc func buttonPressed() { @objc func buttonPressed() {
if let peer = self.peer { if let peer = self.peer {
self.openPeer(EnginePeer(peer)) self.openPeer(peer)
} }
} }

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
@ -29,7 +28,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
private var currentSize: CGSize? private var currentSize: CGSize?
private var fetchStatus: MediaResourceStatus? private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchedDisposable = MetaDisposable() private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable() private var statusDisposable = MetaDisposable()
@ -47,17 +46,19 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
self.openMedia = openMedia self.openMedia = openMedia
var imageReference: ImageMediaReference? var imageReference: ImageMediaReference?
if let file = media.media as? TelegramMediaFile, let presentation = smallestImageRepresentation(file.previewRepresentations) { if case let .file(file) = media.media, let presentation = smallestImageRepresentation(file.previewRepresentations) {
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) let image = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
} }
var streamVideo = false var streamVideo = false
if let file = media.media as? TelegramMediaFile { var fileValue: TelegramMediaFile?
if case let .file(file) = media.media {
streamVideo = isMediaStreamable(media: file) streamVideo = isMediaStreamable(media: file)
fileValue = file
} }
self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true) self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true)
self.videoNode.isUserInteractionEnabled = false self.videoNode.isUserInteractionEnabled = false
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6)) self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
@ -66,13 +67,13 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
self.addSubnode(self.videoNode) self.addSubnode(self.videoNode)
if let file = media.media as? TelegramMediaFile { if case let .file(file) = media.media {
self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start()) self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start())
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch { displayLinkDispatcher.dispatch {
if let strongSelf = self { if let strongSelf = self {
strongSelf.fetchStatus = status strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus() strongSelf.updateFetchStatus()
} }
} }

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import AccountContext import AccountContext

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import Postbox
import TelegramCore import TelegramCore
import SafariServices import SafariServices
import TelegramPresentationData import TelegramPresentationData
@ -197,9 +196,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
if self.contentNode == nil || self.contentNode?.frame.width != width { if self.contentNode == nil || self.contentNode?.frame.width != width {
self.contentNode?.removeFromSupernode() self.contentNode?.removeFromSupernode()
var media: [MediaId: Media] = [:] var media: [EngineMedia.Id: EngineMedia] = [:]
if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage { if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage {
media = instantPage.media media = instantPage.media.mapValues(EngineMedia.init)
} }
let sideInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import TelegramCore import TelegramCore
import Postbox
import Display import Display
import TelegramPresentationData import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import Postbox
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -186,9 +186,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe
private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode { private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode {
let media = self.items[index] let media = self.items[index]
let contentNode: ASDisplayNode let contentNode: ASDisplayNode
if let _ = media.media as? TelegramMediaImage { if case .image = media.media {
contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished) contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished)
} else if let _ = media.media as? TelegramMediaFile { } else if case .file = media.media {
contentNode = ASDisplayNode() contentNode = ASDisplayNode()
} else { } else {
contentNode = ASDisplayNode() contentNode = ASDisplayNode()

View File

@ -1,7 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import SwiftSignalKit import SwiftSignalKit
import Postbox
import TelegramCore import TelegramCore
import TelegramUIPreferences import TelegramUIPreferences
@ -58,7 +57,7 @@ public final class InstantPageStoredState: Codable {
} }
public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal<InstantPageStoredState?, NoError> { public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal<InstantPageStoredState?, NoError> {
let key = ValueBoxKey(length: 8) let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: webPage.webpageId.id) key.setInt64(0, value: webPage.webpageId.id)
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, id: key)) return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, id: key))
@ -68,7 +67,7 @@ public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMedi
} }
public func updateInstantPageStoredStateInteractively(engine: TelegramEngine, webPage: TelegramMediaWebpage, state: InstantPageStoredState?) -> Signal<Never, NoError> { public func updateInstantPageStoredStateInteractively(engine: TelegramEngine, webPage: TelegramMediaWebpage, state: InstantPageStoredState?) -> Signal<Never, NoError> {
let key = ValueBoxKey(length: 8) let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: webPage.webpageId.id) key.setInt64(0, value: webPage.webpageId.id)
if let state = state { if let state = state {

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import AsyncDisplayKit import AsyncDisplayKit
import TelegramCore import TelegramCore
import Postbox
import Display import Display
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
@ -277,7 +276,7 @@ private func offestForVerticalAlignment(_ verticalAlignment: TableVerticalAlignm
} }
} }
func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: InstantPageTextStyleStack, theme: InstantPageTheme, bordered: Bool, striped: Bool, boundingWidth: CGFloat, horizontalInset: CGFloat, media: [MediaId: Media], webpage: TelegramMediaWebpage) -> InstantPageTableItem { func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: InstantPageTextStyleStack, theme: InstantPageTheme, bordered: Bool, striped: Bool, boundingWidth: CGFloat, horizontalInset: CGFloat, media: [EngineMedia.Id: EngineMedia], webpage: TelegramMediaWebpage) -> InstantPageTableItem {
if rows.count == 0 { if rows.count == 0 {
return InstantPageTableItem(frame: CGRect(), totalWidth: 0.0, horizontalInset: 0.0, borderWidth: 0.0, theme: theme, cells: [], rtl: rtl) return InstantPageTableItem(frame: CGRect(), totalWidth: 0.0, horizontalInset: 0.0, borderWidth: 0.0, theme: theme, cells: [], rtl: rtl)
} }

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit import UIKit
import TelegramCore import TelegramCore
import Display import Display
import Postbox
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
@ -12,9 +11,9 @@ import ContextUI
public final class InstantPageUrlItem: Equatable { public final class InstantPageUrlItem: Equatable {
public let url: String public let url: String
public let webpageId: MediaId? public let webpageId: EngineMedia.Id?
public init(url: String, webpageId: MediaId?) { public init(url: String, webpageId: EngineMedia.Id?) {
self.url = url self.url = url
self.webpageId = webpageId self.webpageId = webpageId
} }
@ -36,7 +35,7 @@ struct InstantPageTextStrikethroughItem {
struct InstantPageTextImageItem { struct InstantPageTextImageItem {
let frame: CGRect let frame: CGRect
let range: NSRange let range: NSRange
let id: MediaId let id: EngineMedia.Id
} }
struct InstantPageTextAnchorItem { struct InstantPageTextAnchorItem {
@ -649,7 +648,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
} }
} }
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [MediaId: Media] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) { func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [EngineMedia.Id: EngineMedia] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) {
if string.length == 0 { if string.length == 0 {
return (nil, [], CGSize()) return (nil, [], CGSize())
} }
@ -771,7 +770,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing)) extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
} }
maxImageHeight = max(maxImageHeight, imageFrame.height) maxImageHeight = max(maxImageHeight, imageFrame.height)
lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: MediaId(namespace: Namespaces.Media.CloudFile, id: id))) lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: id)))
} }
} }
} }
@ -876,8 +875,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
for line in textItem.lines { for line in textItem.lines {
let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment) let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
for imageItem in line.imageItems { for imageItem in line.imageItems {
if let image = media[imageItem.id] as? TelegramMediaFile { if case let .image(image) = media[imageItem.id] {
let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: image, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false) let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: .image(image), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
additionalItems.append(item) additionalItems.append(item)
if item.frame.minY < topInset { if item.frame.minY < topInset {

View File

@ -1,6 +1,5 @@
import Foundation import Foundation
import UIKit import UIKit
import Postbox
import TelegramCore import TelegramCore
import AsyncDisplayKit import AsyncDisplayKit
import TelegramPresentationData import TelegramPresentationData

View File

@ -389,7 +389,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context) let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
avatarListWrapperNode.contentNode.clipsToBounds = true avatarListWrapperNode.contentNode.clipsToBounds = true
avatarListNode.backgroundColor = .clear avatarListNode.backgroundColor = .clear
avatarListNode.peer = peer avatarListNode.peer = EnginePeer(peer)
avatarListNode.firstFullSizeOnly = true avatarListNode.firstFullSizeOnly = true
avatarListNode.offsetLocation = true avatarListNode.offsetLocation = true
avatarListNode.customCenterTapAction = { [weak self] in avatarListNode.customCenterTapAction = { [weak self] in
@ -405,7 +405,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode) avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode) avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode)
avatarListNode.update(size: targetRect.size, peer: peer, customNode: nil, additionalEntry: .single(nil), isExpanded: true, transition: .immediate) avatarListNode.update(size: targetRect.size, peer: EnginePeer(peer), customNode: nil, additionalEntry: .single(nil), isExpanded: true, transition: .immediate)
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode) strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
strongSelf.avatarListWrapperNode = avatarListWrapperNode strongSelf.avatarListWrapperNode = avatarListWrapperNode

View File

@ -878,7 +878,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
if statusUpdated && item.displayFileInfo { if statusUpdated && item.displayFileInfo {
if let file = selectedMedia as? TelegramMediaFile { if let file = selectedMedia as? TelegramMediaFile {
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList) updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in |> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus, !item.isDownloadList { if case .Fetching = value.fetchStatus, !item.isDownloadList {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
@ -905,10 +905,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
} }
} }
if isVoice { if isVoice {
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList) updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
} }
} else if let image = selectedMedia as? TelegramMediaImage { } else if let image = selectedMedia as? TelegramMediaImage {
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList) updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in |> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus, !item.isDownloadList { if case .Fetching = value.fetchStatus, !item.isDownloadList {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())

View File

@ -1,156 +0,0 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "LottieMeshSwiftMetalResources",
srcs = glob([
"Resources/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "LottieMeshSwiftBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.LottieMeshSwift</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>LottieMeshSwift</string>
"""
)
apple_resource_bundle(
name = "LottieMeshSwiftBundle",
infoplists = [
":LottieMeshSwiftBundleInfoPlist",
],
resources = [
":LottieMeshSwiftMetalResources",
],
)
config_setting(
name = "debug_build",
values = {
"compilation_mode": "dbg",
},
)
optimization_flags = select({
":debug_build": [
"-O2",
],
"//conditions:default": [],
})
swift_optimization_flags = select({
":debug_build": [
#"-O",
],
"//conditions:default": [],
})
swift_library(
name = "LottieMeshSwift",
module_name = "LottieMeshSwift",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
] + swift_optimization_flags,
data = [
":LottieMeshSwiftBundle",
],
deps = [
":LottieMeshBinding",
"//submodules/Postbox:Postbox",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",
],
)
objc_library(
name = "LottieMeshBinding",
enable_modules = True,
module_name = "LottieMeshBinding",
srcs = glob([
"LottieMeshBinding/Sources/**/*.m",
"LottieMeshBinding/Sources/**/*.mm",
"LottieMeshBinding/Sources/**/*.h",
]),
copts = optimization_flags,
hdrs = glob([
"LottieMeshBinding/PublicHeaders/**/*.h",
]),
includes = [
"LottieMeshBinding/PublicHeaders",
],
deps = [
":LottieMesh",
],
sdk_frameworks = [
"Foundation",
],
visibility = [
"//visibility:public",
],
)
cc_library(
name = "LottieMesh",
srcs = glob([
"LottieMesh/Sources/**/*.cpp",
"LottieMesh/Sources/**/*.h",
"LottieMesh/Sources/**/*.hpp",
]),
copts = [
"-Isubmodules/LottieMeshSwift/libtess2/Include",
] + optimization_flags,
hdrs = glob([
"LottieMesh/PublicHeaders/**/*.h",
"LottieMesh/PublicHeaders/**/*.hpp",
]),
includes = [
"LottieMesh/PublicHeaders",
],
deps = [
":libtess2",
],
visibility = [
"//visibility:public",
],
)
cc_library(
name = "libtess2",
srcs = glob([
"libtess2/Sources/**/*.c",
"libtess2/Sources/**/*.h",
"libtess2/Include/**/*.h",
]),
copts = [
"-Isubmodules/LottieMeshSwift/libtess2/Include",
] + optimization_flags,
hdrs = glob([
"libtess2/Include/**/*.h",
]),
deps = [
],
visibility = [
"//visibility:public",
],
)

View File

@ -1,60 +0,0 @@
#ifndef LottieMesh_h
#define LottieMesh_h
#include <memory>
#include <vector>
#include "Point.h"
namespace MeshGenerator {
struct Path {
std::vector<Point> points;
};
struct Fill {
enum class Rule {
EvenOdd,
NonZero
};
Rule rule = Rule::EvenOdd;
explicit Fill(Rule rule_) :
rule(rule_) {
}
};
struct Stroke {
enum class LineJoin {
Miter,
Round,
Bevel
};
enum class LineCap {
Butt,
Round,
Square
};
float lineWidth = 0.0f;
LineJoin lineJoin = LineJoin::Round;
LineCap lineCap = LineCap::Round;
float miterLimit = 10.0f;
explicit Stroke(float lineWidth_, LineJoin lineJoin_, LineCap lineCap_, float miterLimit_) :
lineWidth(lineWidth_), lineJoin(lineJoin_), lineCap(lineCap_), miterLimit(miterLimit_) {
}
};
struct Mesh {
std::vector<Point> vertices;
std::vector<int> triangles;
};
std::unique_ptr<Mesh> generateMesh(std::vector<Path> const &paths, std::unique_ptr<Fill> fill, std::unique_ptr<Stroke> stroke);
}
#endif /* LottieMesh_h */

View File

@ -1,47 +0,0 @@
#ifndef Point_h
#define Point_h
#include <math.h>
#include <cmath>
namespace MeshGenerator {
struct Point {
float x = 0.0f;
float y = 0.0f;
Point(float x_, float y_) :
x(x_), y(y_) {
}
Point() : Point(0.0f, 0.0f) {
}
bool isEqual(Point const &other, float epsilon = 0.0001f) const {
return std::abs(x - other.x) <= epsilon && std::abs(y - other.y) <= epsilon;
}
float distance(Point const &other) const {
float dx = x - other.x;
float dy = y - other.y;
return sqrtf(dx * dx + dy * dy);
}
bool operator< (Point const &other) const {
if (x < other.x) {
return true;
}
if (x > other.x) {
return false;
}
return y < other.y;
}
bool operator== (Point const &other) const {
return isEqual(other);
}
};
}
#endif

View File

@ -1,86 +0,0 @@
#pragma once
#include "Vec2.h"
#include <optional>
namespace crushedpixel {
template<typename Vec2>
struct LineSegment {
LineSegment(const Vec2 &a, const Vec2 &b) :
a(a), b(b) {}
Vec2 a, b;
/**
* @return A copy of the line segment, offset by the given vector.
*/
LineSegment operator+(const Vec2 &toAdd) const {
return {Vec2Maths::add(a, toAdd), Vec2Maths::add(b, toAdd)};
}
/**
* @return A copy of the line segment, offset by the given vector.
*/
LineSegment operator-(const Vec2 &toRemove) const {
return {Vec2Maths::subtract(a, toRemove), Vec2Maths::subtract(b, toRemove)};
}
/**
* @return The line segment's normal vector.
*/
Vec2 normal() const {
auto dir = direction();
// return the direction vector
// rotated by 90 degrees counter-clockwise
return {-dir.y, dir.x};
}
/**
* @return The line segment's direction vector.
*/
Vec2 direction(bool normalized = true) const {
auto vec = Vec2Maths::subtract(b, a);
return normalized
? Vec2Maths::normalized(vec)
: vec;
}
static Vec2 intersection(const LineSegment &a, const LineSegment &b, bool infiniteLines, bool &success) {
success = true;
// calculate un-normalized direction vectors
auto r = a.direction(false);
auto s = b.direction(false);
auto originDist = Vec2Maths::subtract(b.a, a.a);
auto uNumerator = Vec2Maths::cross(originDist, r);
auto denominator = Vec2Maths::cross(r, s);
if (std::abs(denominator) < 0.0001f) {
// The lines are parallel
success = false;
return Vec2();
}
// solve the intersection positions
auto u = uNumerator / denominator;
auto t = Vec2Maths::cross(originDist, s) / denominator;
if (!infiniteLines && (t < 0 || t > 1 || u < 0 || u > 1)) {
// the intersection lies outside of the line segments
success = false;
return Vec2();
}
// calculate the intersection point
// a.a + r * t;
return Vec2Maths::add(a.a, Vec2Maths::multiply(r, t));
}
};
} // namespace crushedpixel

View File

@ -1,95 +0,0 @@
#include <LottieMesh/LottieMesh.h>
#include <LottieMesh/Point.h>
#include "Triangulation.h"
#include "tesselator.h"
#include "Polyline2D.h"
namespace MeshGenerator {
std::unique_ptr<Mesh> generateMesh(std::vector<Path> const &paths, std::unique_ptr<Fill> fill, std::unique_ptr<Stroke> stroke) {
if (stroke) {
std::unique_ptr<Mesh> mesh = std::make_unique<Mesh>();
for (const auto &path : paths) {
crushedpixel::Polyline2D::JointStyle jointStyle = crushedpixel::Polyline2D::JointStyle::ROUND;
crushedpixel::Polyline2D::EndCapStyle endCapStyle = crushedpixel::Polyline2D::EndCapStyle::SQUARE;
switch (stroke->lineJoin) {
case Stroke::LineJoin::Miter:
jointStyle = crushedpixel::Polyline2D::JointStyle::MITER;
break;
case Stroke::LineJoin::Round:
jointStyle = crushedpixel::Polyline2D::JointStyle::ROUND;
break;
case Stroke::LineJoin::Bevel:
jointStyle = crushedpixel::Polyline2D::JointStyle::BEVEL;
break;
default: {
break;
}
}
switch (stroke->lineCap) {
case Stroke::LineCap::Round: {
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::ROUND;
break;
}
case Stroke::LineCap::Square: {
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::SQUARE;
break;
}
case Stroke::LineCap::Butt: {
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::BUTT;
break;
}
default: {
break;
}
}
auto vertices = crushedpixel::Polyline2D::create(path.points, stroke->lineWidth, jointStyle, endCapStyle);
for (const auto &vertex : vertices) {
mesh->triangles.push_back((int)mesh->vertices.size());
mesh->vertices.push_back(vertex);
}
}
assert(mesh->triangles.size() % 3 == 0);
return mesh;
} else if (fill) {
TESStesselator *tessellator = tessNewTess(NULL);
tessSetOption(tessellator, TESS_CONSTRAINED_DELAUNAY_TRIANGULATION, 1);
for (const auto &path : paths) {
tessAddContour(tessellator, 2, path.points.data(), sizeof(Point), (int)path.points.size());
}
switch (fill->rule) {
case Fill::Rule::EvenOdd: {
tessTesselate(tessellator, TESS_WINDING_ODD, TESS_POLYGONS, 3, 2, NULL);
break;
}
default: {
tessTesselate(tessellator, TESS_WINDING_NONZERO, TESS_POLYGONS, 3, 2, NULL);
break;
}
}
int vertexCount = tessGetVertexCount(tessellator);
const TESSreal *vertices = tessGetVertices(tessellator);
int indexCount = tessGetElementCount(tessellator) * 3;
const TESSindex *indices = tessGetElements(tessellator);
std::unique_ptr<Mesh> mesh = std::make_unique<Mesh>();
for (int i = 0; i < vertexCount; i++) {
mesh->vertices.push_back(Point(vertices[i * 2 + 0], vertices[i * 2 + 1]));
}
for (int i = 0; i < indexCount; i++) {
mesh->triangles.push_back(indices[i]);
}
return mesh;
} else {
return nullptr;
}
}
}

View File

@ -1,446 +0,0 @@
#pragma once
#include "LineSegment.h"
#include <vector>
#include <iterator>
#include <cassert>
namespace crushedpixel {
class Polyline2D {
public:
enum class JointStyle {
/**
* Corners are drawn with sharp joints.
* If the joint's outer angle is too large,
* the joint is drawn as beveled instead,
* to avoid the miter extending too far out.
*/
MITER,
/**
* Corners are flattened.
*/
BEVEL,
/**
* Corners are rounded off.
*/
ROUND
};
enum class EndCapStyle {
/**
* Path ends are drawn flat,
* and don't exceed the actual end point.
*/
BUTT, // lol
/**
* Path ends are drawn flat,
* but extended beyond the end point
* by half the line thickness.
*/
SQUARE,
/**
* Path ends are rounded off.
*/
ROUND,
/**
* Path ends are connected according to the JointStyle.
* When using this EndCapStyle, don't specify the common start/end point twice,
* as Polyline2D connects the first and last input point itself.
*/
JOINT
};
/**
* Creates a vector of vertices describing a solid path through the input points.
* @param points The points of the path.
* @param thickness The path's thickness.
* @param jointStyle The path's joint style.
* @param endCapStyle The path's end cap style.
* @param allowOverlap Whether to allow overlapping vertices.
* This yields better results when dealing with paths
* whose points have a distance smaller than the thickness,
* but may introduce overlapping vertices,
* which is undesirable when rendering transparent paths.
* @return The vertices describing the path.
* @tparam Vec2 The vector type to use for the vertices.
* Must have public non-const float fields "x" and "y".
* Must have a two-args constructor taking x and y values.
* See crushedpixel::Vec2 for a type that satisfies these requirements.
* @tparam InputCollection The collection type of the input points.
* Must contain elements of type Vec2.
* Must expose size() and operator[] functions.
*/
template<typename Vec2, typename InputCollection>
static std::vector<Vec2> create(const InputCollection &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
std::vector<Vec2> vertices;
create(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap);
return vertices;
}
template<typename Vec2>
static std::vector<Vec2> create(const std::vector<Vec2> &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
std::vector<Vec2> vertices;
create<Vec2, std::vector<Vec2>>(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap);
return vertices;
}
template<typename Vec2, typename InputCollection>
static size_t create(std::vector<Vec2> &vertices, const InputCollection &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
auto numVerticesBefore = vertices.size();
create<Vec2, InputCollection>(std::back_inserter(vertices), points, thickness,
jointStyle, endCapStyle, allowOverlap);
return vertices.size() - numVerticesBefore;
}
template<typename Vec2, typename InputCollection, typename OutputIterator>
static OutputIterator create(OutputIterator vertices, const InputCollection &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
// operate on half the thickness to make our lives easier
thickness /= 2;
// create poly segments from the points
std::vector<PolySegment<Vec2>> segments;
for (size_t i = 0; i + 1 < points.size(); i++) {
auto &point1 = points[i];
auto &point2 = points[i + 1];
// to avoid division-by-zero errors,
// only create a line segment for non-identical points
if (!Vec2Maths::equal(point1, point2)) {
segments.emplace_back(LineSegment<Vec2>(point1, point2), thickness);
}
}
if (endCapStyle == EndCapStyle::JOINT) {
// create a connecting segment from the last to the first point
auto &point1 = points[points.size() - 1];
auto &point2 = points[0];
// to avoid division-by-zero errors,
// only create a line segment for non-identical points
if (!Vec2Maths::equal(point1, point2)) {
segments.emplace_back(LineSegment<Vec2>(point1, point2), thickness);
}
}
if (segments.empty()) {
// handle the case of insufficient input points
return vertices;
}
Vec2 nextStart1{0, 0};
Vec2 nextStart2{0, 0};
Vec2 start1{0, 0};
Vec2 start2{0, 0};
Vec2 end1{0, 0};
Vec2 end2{0, 0};
// calculate the path's global start and end points
auto &firstSegment = segments[0];
auto &lastSegment = segments[segments.size() - 1];
auto pathStart1 = firstSegment.edge1.a;
auto pathStart2 = firstSegment.edge2.a;
auto pathEnd1 = lastSegment.edge1.b;
auto pathEnd2 = lastSegment.edge2.b;
// handle different end cap styles
if (endCapStyle == EndCapStyle::SQUARE) {
// extend the start/end points by half the thickness
pathStart1 = Vec2Maths::subtract(pathStart1, Vec2Maths::multiply(firstSegment.edge1.direction(), thickness));
pathStart2 = Vec2Maths::subtract(pathStart2, Vec2Maths::multiply(firstSegment.edge2.direction(), thickness));
pathEnd1 = Vec2Maths::add(pathEnd1, Vec2Maths::multiply(lastSegment.edge1.direction(), thickness));
pathEnd2 = Vec2Maths::add(pathEnd2, Vec2Maths::multiply(lastSegment.edge2.direction(), thickness));
} else if (endCapStyle == EndCapStyle::ROUND) {
// draw half circle end caps
createTriangleFan(vertices, firstSegment.center.a, firstSegment.center.a,
firstSegment.edge1.a, firstSegment.edge2.a, false);
createTriangleFan(vertices, lastSegment.center.b, lastSegment.center.b,
lastSegment.edge1.b, lastSegment.edge2.b, true);
} else if (endCapStyle == EndCapStyle::JOINT) {
// join the last (connecting) segment and the first segment
createJoint(vertices, lastSegment, firstSegment, jointStyle,
pathEnd1, pathEnd2, pathStart1, pathStart2, allowOverlap);
}
// generate mesh data for path segments
for (size_t i = 0; i < segments.size(); i++) {
auto &segment = segments[i];
// calculate start
if (i == 0) {
// this is the first segment
start1 = pathStart1;
start2 = pathStart2;
}
if (i + 1 == segments.size()) {
// this is the last segment
end1 = pathEnd1;
end2 = pathEnd2;
} else {
createJoint(vertices, segment, segments[i + 1], jointStyle,
end1, end2, nextStart1, nextStart2, allowOverlap);
}
// emit vertices
*vertices++ = start1;
*vertices++ = start2;
*vertices++ = end1;
*vertices++ = end1;
*vertices++ = start2;
*vertices++ = end2;
start1 = nextStart1;
start2 = nextStart2;
}
return vertices;
}
private:
static constexpr float pi = 3.14159265358979323846f;
/**
* The threshold for mitered joints.
* If the joint's angle is smaller than this angle,
* the joint will be drawn beveled instead.
*/
static constexpr float miterMinAngle = 0.349066; // ~20 degrees
/**
* The minimum angle of a round joint's triangles.
*/
static constexpr float roundMinAngle = 0.174533; // ~10 degrees
template<typename Vec2>
struct PolySegment {
PolySegment(const LineSegment<Vec2> &center, float thickness) :
center(center),
// calculate the segment's outer edges by offsetting
// the central line by the normal vector
// multiplied with the thickness
// center + center.normal() * thickness
edge1(center + Vec2Maths::multiply(center.normal(), thickness)),
edge2(center - Vec2Maths::multiply(center.normal(), thickness)) {}
LineSegment<Vec2> center, edge1, edge2;
};
template<typename Vec2, typename OutputIterator>
static OutputIterator createJoint(OutputIterator vertices,
const PolySegment<Vec2> &segment1, const PolySegment<Vec2> &segment2,
JointStyle jointStyle, Vec2 &end1, Vec2 &end2,
Vec2 &nextStart1, Vec2 &nextStart2,
bool allowOverlap) {
// calculate the angle between the two line segments
auto dir1 = segment1.center.direction();
auto dir2 = segment2.center.direction();
auto angle = Vec2Maths::angle(dir1, dir2);
// wrap the angle around the 180° mark if it exceeds 90°
// for minimum angle detection
auto wrappedAngle = angle;
if (wrappedAngle > pi / 2) {
wrappedAngle = pi - wrappedAngle;
}
if (jointStyle == JointStyle::MITER && wrappedAngle < miterMinAngle) {
// the minimum angle for mitered joints wasn't exceeded.
// to avoid the intersection point being extremely far out,
// thus producing an enormous joint like a rasta on 4/20,
// we render the joint beveled instead.
jointStyle = JointStyle::BEVEL;
}
if (jointStyle == JointStyle::MITER) {
// calculate each edge's intersection point
// with the next segment's central line
bool sec1Success = true;
bool sec2Success = true;
auto sec1 = LineSegment<Vec2>::intersection(segment1.edge1, segment2.edge1, true, sec1Success);
auto sec2 = LineSegment<Vec2>::intersection(segment1.edge2, segment2.edge2, true, sec2Success);
end1 = sec1Success ? sec1 : segment1.edge1.b;
end2 = sec2Success ? sec2 : segment1.edge2.b;
nextStart1 = end1;
nextStart2 = end2;
} else {
// joint style is either BEVEL or ROUND
// find out which are the inner edges for this joint
auto x1 = dir1.x;
auto x2 = dir2.x;
auto y1 = dir1.y;
auto y2 = dir2.y;
auto clockwise = x1 * y2 - x2 * y1 < 0;
const LineSegment<Vec2> *inner1, *inner2, *outer1, *outer2;
// as the normal vector is rotated counter-clockwise,
// the first edge lies to the left
// from the central line's perspective,
// and the second one to the right.
if (clockwise) {
outer1 = &segment1.edge1;
outer2 = &segment2.edge1;
inner1 = &segment1.edge2;
inner2 = &segment2.edge2;
} else {
outer1 = &segment1.edge2;
outer2 = &segment2.edge2;
inner1 = &segment1.edge1;
inner2 = &segment2.edge1;
}
// calculate the intersection point of the inner edges
bool innerSecOptSuccess = true;
auto innerSecOpt = LineSegment<Vec2>::intersection(*inner1, *inner2, allowOverlap, innerSecOptSuccess);
auto innerSec = innerSecOptSuccess
? innerSecOpt
// for parallel lines, simply connect them directly
: inner1->b;
// if there's no inner intersection, flip
// the next start position for near-180° turns
Vec2 innerStart;
if (innerSecOptSuccess) {
innerStart = innerSec;
} else if (angle > pi / 2) {
innerStart = outer1->b;
} else {
innerStart = inner1->b;
}
if (clockwise) {
end1 = outer1->b;
end2 = innerSec;
nextStart1 = outer2->a;
nextStart2 = innerStart;
} else {
end1 = innerSec;
end2 = outer1->b;
nextStart1 = innerStart;
nextStart2 = outer2->a;
}
// connect the intersection points according to the joint style
if (jointStyle == JointStyle::BEVEL) {
// simply connect the intersection points
*vertices++ = outer1->b;
*vertices++ = outer2->a;
*vertices++ = innerSec;
} else if (jointStyle == JointStyle::ROUND) {
// draw a circle between the ends of the outer edges,
// centered at the actual point
// with half the line thickness as the radius
createTriangleFan(vertices, innerSec, segment1.center.b, outer1->b, outer2->a, clockwise);
} else {
assert(false);
}
}
return vertices;
}
/**
* Creates a partial circle between two points.
* The points must be equally far away from the origin.
* @param vertices The vector to add vertices to.
* @param connectTo The position to connect the triangles to.
* @param origin The circle's origin.
* @param start The circle's starting point.
* @param end The circle's ending point.
* @param clockwise Whether the circle's rotation is clockwise.
*/
template<typename Vec2, typename OutputIterator>
static OutputIterator createTriangleFan(OutputIterator vertices, Vec2 connectTo, Vec2 origin,
Vec2 start, Vec2 end, bool clockwise) {
auto point1 = Vec2Maths::subtract(start, origin);
auto point2 = Vec2Maths::subtract(end, origin);
// calculate the angle between the two points
auto angle1 = atan2(point1.y, point1.x);
auto angle2 = atan2(point2.y, point2.x);
// ensure the outer angle is calculated
if (clockwise) {
if (angle2 > angle1) {
angle2 = angle2 - 2 * pi;
}
} else {
if (angle1 > angle2) {
angle1 = angle1 - 2 * pi;
}
}
auto jointAngle = angle2 - angle1;
// calculate the amount of triangles to use for the joint
auto numTriangles = std::max(1, (int) std::floor(std::abs(jointAngle) / roundMinAngle));
// calculate the angle of each triangle
auto triAngle = jointAngle / numTriangles;
Vec2 startPoint = start;
Vec2 endPoint;
for (int t = 0; t < numTriangles; t++) {
if (t + 1 == numTriangles) {
// it's the last triangle - ensure it perfectly
// connects to the next line
endPoint = end;
} else {
auto rot = (t + 1) * triAngle;
// rotate the original point around the origin
endPoint.x = std::cos(rot) * point1.x - std::sin(rot) * point1.y;
endPoint.y = std::sin(rot) * point1.x + std::cos(rot) * point1.y;
// re-add the rotation origin to the target point
endPoint = Vec2Maths::add(endPoint, origin);
}
// emit the triangle
*vertices++ = startPoint;
*vertices++ = endPoint;
*vertices++ = connectTo;
startPoint = endPoint;
}
return vertices;
}
};
} // namespace crushedpixel

View File

@ -1,51 +0,0 @@
#include "Triangulation.h"
#include <map>
#include <array>
#include "earcut.hpp"
namespace MeshGenerator {
std::vector<uint32_t> triangulatePolygon(std::vector<Point> const &points, std::vector<int> &indices, std::vector<std::vector<int>> const &holeIndices) {
// The index type. Defaults to uint32_t, but you can also pass uint16_t if you know that your
// data won't have more than 65536 vertices.
using N = uint32_t;
// Create array
using EarPoint = std::array<float, 2>;
std::vector<std::vector<EarPoint>> polygon;
std::map<int, int> facePointMapping;
int nextFacePointIndex = 0;
std::vector<EarPoint> facePoints;
for (auto index : indices) {
facePointMapping[nextFacePointIndex] = index;
nextFacePointIndex++;
facePoints.push_back({ points[index].x, points[index].y });
}
polygon.push_back(std::move(facePoints));
for (const auto &list : holeIndices) {
std::vector<EarPoint> holePoints;
for (auto index : list) {
facePointMapping[nextFacePointIndex] = index;
nextFacePointIndex++;
holePoints.push_back({ points[index].x, points[index].y });
}
polygon.push_back(std::move(holePoints));
}
std::vector<N> triangleIndices = mapbox::earcut<N>(polygon);
std::vector<uint32_t> mappedIndices;
for (auto index : triangleIndices) {
mappedIndices.push_back(facePointMapping[index]);
}
return mappedIndices;
}
}

View File

@ -1,14 +0,0 @@
#ifndef Triangulation_h
#define Triangulation_h
#include <vector>
#include <LottieMesh/Point.h>
namespace MeshGenerator {
std::vector<uint32_t> triangulatePolygon(std::vector<Point> const &points, std::vector<int> &indices, std::vector<std::vector<int>> const &holeIndices);
}
#endif /* Triangulation_h */

View File

@ -1,99 +0,0 @@
#pragma once
#include <cmath>
namespace crushedpixel {
/**
* A two-dimensional float vector.
* It exposes the x and y fields
* as required by the Polyline2D functions.
*/
struct Vec2 {
Vec2() :
Vec2(0, 0) {}
Vec2(float x, float y) :
x(x), y(y) {}
virtual ~Vec2() = default;
float x, y;
};
namespace Vec2Maths {
template<typename Vec2>
static bool equal(const Vec2 &a, const Vec2 &b) {
return a.x == b.x && a.y == b.y;
}
template<typename Vec2>
static Vec2 multiply(const Vec2 &a, const Vec2 &b) {
return {a.x * b.x, a.y * b.y};
}
template<typename Vec2>
static Vec2 multiply(const Vec2 &vec, float factor) {
return {vec.x * factor, vec.y * factor};
}
template<typename Vec2>
static Vec2 divide(const Vec2 &vec, float factor) {
return {vec.x / factor, vec.y / factor};
}
template<typename Vec2>
static Vec2 add(const Vec2 &a, const Vec2 &b) {
return {a.x + b.x, a.y + b.y};
}
template<typename Vec2>
static Vec2 subtract(const Vec2 &a, const Vec2 &b) {
return {a.x - b.x, a.y - b.y};
}
template<typename Vec2>
static float magnitude(const Vec2 &vec) {
return std::sqrt(vec.x * vec.x + vec.y * vec.y);
}
template<typename Vec2>
static Vec2 withLength(const Vec2 &vec, float len) {
auto mag = magnitude(vec);
auto factor = mag / len;
return divide(vec, factor);
}
template<typename Vec2>
static Vec2 normalized(const Vec2 &vec) {
return withLength(vec, 1);
}
/**
* Calculates the dot product of two vectors.
*/
template<typename Vec2>
static float dot(const Vec2 &a, const Vec2 &b) {
return a.x * b.x + a.y * b.y;
}
/**
* Calculates the cross product of two vectors.
*/
template<typename Vec2>
static float cross(const Vec2 &a, const Vec2 &b) {
return a.x * b.y - a.y * b.x;
}
/**
* Calculates the angle between two vectors.
*/
template<typename Vec2>
static float angle(const Vec2 &a, const Vec2 &b) {
return std::acos(dot(a, b) / (magnitude(a) * magnitude(b)));
}
} // namespace Vec2Maths
}

View File

@ -1,823 +0,0 @@
#pragma once
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <utility>
#include <vector>
namespace mapbox {
namespace util {
template <std::size_t I, typename T> struct nth {
inline static typename std::tuple_element<I, T>::type
get(const T& t) { return std::get<I>(t); };
};
}
namespace detail {
template <typename N = uint32_t>
class Earcut {
public:
std::vector<N> indices;
std::size_t vertices = 0;
template <typename Polygon>
void operator()(const Polygon& points);
private:
struct Node {
Node(N index, double x_, double y_) : i(index), x(x_), y(y_) {}
Node(const Node&) = delete;
Node& operator=(const Node&) = delete;
Node(Node&&) = delete;
Node& operator=(Node&&) = delete;
const N i;
const double x;
const double y;
// previous and next vertice nodes in a polygon ring
Node* prev = nullptr;
Node* next = nullptr;
// z-order curve value
int32_t z = 0;
// previous and next nodes in z-order
Node* prevZ = nullptr;
Node* nextZ = nullptr;
// indicates whether this is a steiner point
bool steiner = false;
};
template <typename Ring> Node* linkedList(const Ring& points, const bool clockwise);
Node* filterPoints(Node* start, Node* end = nullptr);
void earcutLinked(Node* ear, int pass = 0);
bool isEar(Node* ear);
bool isEarHashed(Node* ear);
Node* cureLocalIntersections(Node* start);
void splitEarcut(Node* start);
template <typename Polygon> Node* eliminateHoles(const Polygon& points, Node* outerNode);
Node* eliminateHole(Node* hole, Node* outerNode);
Node* findHoleBridge(Node* hole, Node* outerNode);
bool sectorContainsSector(const Node* m, const Node* p);
void indexCurve(Node* start);
Node* sortLinked(Node* list);
int32_t zOrder(const double x_, const double y_);
Node* getLeftmost(Node* start);
bool pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const;
bool isValidDiagonal(Node* a, Node* b);
double area(const Node* p, const Node* q, const Node* r) const;
bool equals(const Node* p1, const Node* p2);
bool intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2);
bool onSegment(const Node* p, const Node* q, const Node* r);
int sign(double val);
bool intersectsPolygon(const Node* a, const Node* b);
bool locallyInside(const Node* a, const Node* b);
bool middleInside(const Node* a, const Node* b);
Node* splitPolygon(Node* a, Node* b);
template <typename Point> Node* insertNode(std::size_t i, const Point& p, Node* last);
void removeNode(Node* p);
bool hashing;
double minX, maxX;
double minY, maxY;
double inv_size = 0;
template <typename T, typename Alloc = std::allocator<T>>
class ObjectPool {
public:
ObjectPool() { }
ObjectPool(std::size_t blockSize_) {
reset(blockSize_);
}
~ObjectPool() {
clear();
}
template <typename... Args>
T* construct(Args&&... args) {
if (currentIndex >= blockSize) {
currentBlock = alloc_traits::allocate(alloc, blockSize);
allocations.emplace_back(currentBlock);
currentIndex = 0;
}
T* object = &currentBlock[currentIndex++];
alloc_traits::construct(alloc, object, std::forward<Args>(args)...);
return object;
}
void reset(std::size_t newBlockSize) {
for (auto allocation : allocations) {
alloc_traits::deallocate(alloc, allocation, blockSize);
}
allocations.clear();
blockSize = std::max<std::size_t>(1, newBlockSize);
currentBlock = nullptr;
currentIndex = blockSize;
}
void clear() { reset(blockSize); }
private:
T* currentBlock = nullptr;
std::size_t currentIndex = 1;
std::size_t blockSize = 1;
std::vector<T*> allocations;
Alloc alloc;
typedef typename std::allocator_traits<Alloc> alloc_traits;
};
ObjectPool<Node> nodes;
};
template <typename N> template <typename Polygon>
void Earcut<N>::operator()(const Polygon& points) {
// reset
indices.clear();
vertices = 0;
if (points.empty()) return;
double x;
double y;
int threshold = 80;
std::size_t len = 0;
for (size_t i = 0; threshold >= 0 && i < points.size(); i++) {
threshold -= static_cast<int>(points[i].size());
len += points[i].size();
}
//estimate size of nodes and indices
nodes.reset(len * 3 / 2);
indices.reserve(len + points[0].size());
Node* outerNode = linkedList(points[0], true);
if (!outerNode || outerNode->prev == outerNode->next) return;
if (points.size() > 1) outerNode = eliminateHoles(points, outerNode);
// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
hashing = threshold < 0;
if (hashing) {
Node* p = outerNode->next;
minX = maxX = outerNode->x;
minY = maxY = outerNode->y;
do {
x = p->x;
y = p->y;
minX = std::min<double>(minX, x);
minY = std::min<double>(minY, y);
maxX = std::max<double>(maxX, x);
maxY = std::max<double>(maxY, y);
p = p->next;
} while (p != outerNode);
// minX, minY and size are later used to transform coords into integers for z-order calculation
inv_size = std::max<double>(maxX - minX, maxY - minY);
inv_size = inv_size != .0 ? (1. / inv_size) : .0;
}
earcutLinked(outerNode);
nodes.clear();
}
// create a circular doubly linked list from polygon points in the specified winding order
template <typename N> template <typename Ring>
typename Earcut<N>::Node*
Earcut<N>::linkedList(const Ring& points, const bool clockwise) {
using Point = typename Ring::value_type;
double sum = 0;
const std::size_t len = points.size();
std::size_t i, j;
Node* last = nullptr;
// calculate original winding order of a polygon ring
for (i = 0, j = len > 0 ? len - 1 : 0; i < len; j = i++) {
const auto& p1 = points[i];
const auto& p2 = points[j];
const double p20 = util::nth<0, Point>::get(p2);
const double p10 = util::nth<0, Point>::get(p1);
const double p11 = util::nth<1, Point>::get(p1);
const double p21 = util::nth<1, Point>::get(p2);
sum += (p20 - p10) * (p11 + p21);
}
// link points into circular doubly-linked list in the specified winding order
if (clockwise == (sum > 0)) {
for (i = 0; i < len; i++) last = insertNode(vertices + i, points[i], last);
} else {
for (i = len; i-- > 0;) last = insertNode(vertices + i, points[i], last);
}
if (last && equals(last, last->next)) {
removeNode(last);
last = last->next;
}
vertices += len;
return last;
}
// eliminate colinear or duplicate points
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::filterPoints(Node* start, Node* end) {
if (!end) end = start;
Node* p = start;
bool again;
do {
again = false;
if (!p->steiner && (equals(p, p->next) || area(p->prev, p, p->next) == 0)) {
removeNode(p);
p = end = p->prev;
if (p == p->next) break;
again = true;
} else {
p = p->next;
}
} while (again || p != end);
return end;
}
// main ear slicing loop which triangulates a polygon (given as a linked list)
template <typename N>
void Earcut<N>::earcutLinked(Node* ear, int pass) {
if (!ear) return;
// interlink polygon nodes in z-order
if (!pass && hashing) indexCurve(ear);
Node* stop = ear;
Node* prev;
Node* next;
int iterations = 0;
// iterate through ears, slicing them one by one
while (ear->prev != ear->next) {
iterations++;
prev = ear->prev;
next = ear->next;
if (hashing ? isEarHashed(ear) : isEar(ear)) {
// cut off the triangle
indices.emplace_back(prev->i);
indices.emplace_back(ear->i);
indices.emplace_back(next->i);
removeNode(ear);
// skipping the next vertice leads to less sliver triangles
ear = next->next;
stop = next->next;
continue;
}
ear = next;
// if we looped through the whole remaining polygon and can't find any more ears
if (ear == stop) {
// try filtering points and slicing again
if (!pass) earcutLinked(filterPoints(ear), 1);
// if this didn't work, try curing all small self-intersections locally
else if (pass == 1) {
ear = cureLocalIntersections(filterPoints(ear));
earcutLinked(ear, 2);
// as a last resort, try splitting the remaining polygon into two
} else if (pass == 2) splitEarcut(ear);
break;
}
}
}
// check whether a polygon node forms a valid ear with adjacent nodes
template <typename N>
bool Earcut<N>::isEar(Node* ear) {
const Node* a = ear->prev;
const Node* b = ear;
const Node* c = ear->next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// now make sure we don't have other points inside the potential ear
Node* p = ear->next->next;
while (p != ear->prev) {
if (pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
area(p->prev, p, p->next) >= 0) return false;
p = p->next;
}
return true;
}
template <typename N>
bool Earcut<N>::isEarHashed(Node* ear) {
const Node* a = ear->prev;
const Node* b = ear;
const Node* c = ear->next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// triangle bbox; min & max are calculated like this for speed
const double minTX = std::min<double>(a->x, std::min<double>(b->x, c->x));
const double minTY = std::min<double>(a->y, std::min<double>(b->y, c->y));
const double maxTX = std::max<double>(a->x, std::max<double>(b->x, c->x));
const double maxTY = std::max<double>(a->y, std::max<double>(b->y, c->y));
// z-order range for the current triangle bbox;
const int32_t minZ = zOrder(minTX, minTY);
const int32_t maxZ = zOrder(maxTX, maxTY);
// first look for points inside the triangle in increasing z-order
Node* p = ear->nextZ;
while (p && p->z <= maxZ) {
if (p != ear->prev && p != ear->next &&
pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
area(p->prev, p, p->next) >= 0) return false;
p = p->nextZ;
}
// then look for points in decreasing z-order
p = ear->prevZ;
while (p && p->z >= minZ) {
if (p != ear->prev && p != ear->next &&
pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
area(p->prev, p, p->next) >= 0) return false;
p = p->prevZ;
}
return true;
}
// go through all polygon nodes and cure small local self-intersections
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::cureLocalIntersections(Node* start) {
Node* p = start;
do {
Node* a = p->prev;
Node* b = p->next->next;
// a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2])
if (!equals(a, b) && intersects(a, p, p->next, b) && locallyInside(a, b) && locallyInside(b, a)) {
indices.emplace_back(a->i);
indices.emplace_back(p->i);
indices.emplace_back(b->i);
// remove two nodes involved
removeNode(p);
removeNode(p->next);
p = start = b;
}
p = p->next;
} while (p != start);
return filterPoints(p);
}
// try splitting polygon into two and triangulate them independently
template <typename N>
void Earcut<N>::splitEarcut(Node* start) {
// look for a valid diagonal that divides the polygon into two
Node* a = start;
do {
Node* b = a->next->next;
while (b != a->prev) {
if (a->i != b->i && isValidDiagonal(a, b)) {
// split the polygon in two by the diagonal
Node* c = splitPolygon(a, b);
// filter colinear points around the cuts
a = filterPoints(a, a->next);
c = filterPoints(c, c->next);
// run earcut on each half
earcutLinked(a);
earcutLinked(c);
return;
}
b = b->next;
}
a = a->next;
} while (a != start);
}
// link every hole into the outer loop, producing a single-ring polygon without holes
template <typename N> template <typename Polygon>
typename Earcut<N>::Node*
Earcut<N>::eliminateHoles(const Polygon& points, Node* outerNode) {
const size_t len = points.size();
std::vector<Node*> queue;
for (size_t i = 1; i < len; i++) {
Node* list = linkedList(points[i], false);
if (list) {
if (list == list->next) list->steiner = true;
queue.push_back(getLeftmost(list));
}
}
std::sort(queue.begin(), queue.end(), [](const Node* a, const Node* b) {
return a->x < b->x;
});
// process holes from left to right
for (size_t i = 0; i < queue.size(); i++) {
outerNode = eliminateHole(queue[i], outerNode);
outerNode = filterPoints(outerNode, outerNode->next);
}
return outerNode;
}
// find a bridge between vertices that connects hole with an outer ring and and link it
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::eliminateHole(Node* hole, Node* outerNode) {
Node* bridge = findHoleBridge(hole, outerNode);
if (!bridge) {
return outerNode;
}
Node* bridgeReverse = splitPolygon(bridge, hole);
// filter collinear points around the cuts
Node* filteredBridge = filterPoints(bridge, bridge->next);
filterPoints(bridgeReverse, bridgeReverse->next);
// Check if input node was removed by the filtering
return outerNode == bridge ? filteredBridge : outerNode;
}
// David Eberly's algorithm for finding a bridge between hole and outer polygon
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::findHoleBridge(Node* hole, Node* outerNode) {
Node* p = outerNode;
double hx = hole->x;
double hy = hole->y;
double qx = -std::numeric_limits<double>::infinity();
Node* m = nullptr;
// find a segment intersected by a ray from the hole's leftmost Vertex to the left;
// segment's endpoint with lesser x will be potential connection Vertex
do {
if (hy <= p->y && hy >= p->next->y && p->next->y != p->y) {
double x = p->x + (hy - p->y) * (p->next->x - p->x) / (p->next->y - p->y);
if (x <= hx && x > qx) {
qx = x;
if (x == hx) {
if (hy == p->y) return p;
if (hy == p->next->y) return p->next;
}
m = p->x < p->next->x ? p : p->next;
}
}
p = p->next;
} while (p != outerNode);
if (!m) return 0;
if (hx == qx) return m; // hole touches outer segment; pick leftmost endpoint
// look for points inside the triangle of hole Vertex, segment intersection and endpoint;
// if there are no points found, we have a valid connection;
// otherwise choose the Vertex of the minimum angle with the ray as connection Vertex
const Node* stop = m;
double tanMin = std::numeric_limits<double>::infinity();
double tanCur = 0;
p = m;
double mx = m->x;
double my = m->y;
do {
if (hx >= p->x && p->x >= mx && hx != p->x &&
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p->x, p->y)) {
tanCur = std::abs(hy - p->y) / (hx - p->x); // tangential
if (locallyInside(p, hole) &&
(tanCur < tanMin || (tanCur == tanMin && (p->x > m->x || sectorContainsSector(m, p))))) {
m = p;
tanMin = tanCur;
}
}
p = p->next;
} while (p != stop);
return m;
}
// whether sector in vertex m contains sector in vertex p in the same coordinates
template <typename N>
bool Earcut<N>::sectorContainsSector(const Node* m, const Node* p) {
return area(m->prev, m, p->prev) < 0 && area(p->next, m, m->next) < 0;
}
// interlink polygon nodes in z-order
template <typename N>
void Earcut<N>::indexCurve(Node* start) {
assert(start);
Node* p = start;
do {
p->z = p->z ? p->z : zOrder(p->x, p->y);
p->prevZ = p->prev;
p->nextZ = p->next;
p = p->next;
} while (p != start);
p->prevZ->nextZ = nullptr;
p->prevZ = nullptr;
sortLinked(p);
}
// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::sortLinked(Node* list) {
assert(list);
Node* p;
Node* q;
Node* e;
Node* tail;
int i, numMerges, pSize, qSize;
int inSize = 1;
for (;;) {
p = list;
list = nullptr;
tail = nullptr;
numMerges = 0;
while (p) {
numMerges++;
q = p;
pSize = 0;
for (i = 0; i < inSize; i++) {
pSize++;
q = q->nextZ;
if (!q) break;
}
qSize = inSize;
while (pSize > 0 || (qSize > 0 && q)) {
if (pSize == 0) {
e = q;
q = q->nextZ;
qSize--;
} else if (qSize == 0 || !q) {
e = p;
p = p->nextZ;
pSize--;
} else if (p->z <= q->z) {
e = p;
p = p->nextZ;
pSize--;
} else {
e = q;
q = q->nextZ;
qSize--;
}
if (tail) tail->nextZ = e;
else list = e;
e->prevZ = tail;
tail = e;
}
p = q;
}
tail->nextZ = nullptr;
if (numMerges <= 1) return list;
inSize *= 2;
}
}
// z-order of a Vertex given coords and size of the data bounding box
template <typename N>
int32_t Earcut<N>::zOrder(const double x_, const double y_) {
// coords are transformed into non-negative 15-bit integer range
int32_t x = static_cast<int32_t>(32767.0 * (x_ - minX) * inv_size);
int32_t y = static_cast<int32_t>(32767.0 * (y_ - minY) * inv_size);
x = (x | (x << 8)) & 0x00FF00FF;
x = (x | (x << 4)) & 0x0F0F0F0F;
x = (x | (x << 2)) & 0x33333333;
x = (x | (x << 1)) & 0x55555555;
y = (y | (y << 8)) & 0x00FF00FF;
y = (y | (y << 4)) & 0x0F0F0F0F;
y = (y | (y << 2)) & 0x33333333;
y = (y | (y << 1)) & 0x55555555;
return x | (y << 1);
}
// find the leftmost node of a polygon ring
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::getLeftmost(Node* start) {
Node* p = start;
Node* leftmost = start;
do {
if (p->x < leftmost->x || (p->x == leftmost->x && p->y < leftmost->y))
leftmost = p;
p = p->next;
} while (p != start);
return leftmost;
}
// check if a point lies within a convex triangle
template <typename N>
bool Earcut<N>::pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const {
return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
(ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
(bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
}
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
template <typename N>
bool Earcut<N>::isValidDiagonal(Node* a, Node* b) {
return a->next->i != b->i && a->prev->i != b->i && !intersectsPolygon(a, b) && // dones't intersect other edges
((locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
(area(a->prev, a, b->prev) != 0.0 || area(a, b->prev, b) != 0.0)) || // does not create opposite-facing sectors
(equals(a, b) && area(a->prev, a, a->next) > 0 && area(b->prev, b, b->next) > 0)); // special zero-length case
}
// signed area of a triangle
template <typename N>
double Earcut<N>::area(const Node* p, const Node* q, const Node* r) const {
return (q->y - p->y) * (r->x - q->x) - (q->x - p->x) * (r->y - q->y);
}
// check if two points are equal
template <typename N>
bool Earcut<N>::equals(const Node* p1, const Node* p2) {
return p1->x == p2->x && p1->y == p2->y;
}
// check if two segments intersect
template <typename N>
bool Earcut<N>::intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2) {
int o1 = sign(area(p1, q1, p2));
int o2 = sign(area(p1, q1, q2));
int o3 = sign(area(p2, q2, p1));
int o4 = sign(area(p2, q2, q1));
if (o1 != o2 && o3 != o4) return true; // general case
if (o1 == 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
if (o2 == 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
if (o3 == 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
if (o4 == 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
return false;
}
// for collinear points p, q, r, check if point q lies on segment pr
template <typename N>
bool Earcut<N>::onSegment(const Node* p, const Node* q, const Node* r) {
return q->x <= std::max<double>(p->x, r->x) &&
q->x >= std::min<double>(p->x, r->x) &&
q->y <= std::max<double>(p->y, r->y) &&
q->y >= std::min<double>(p->y, r->y);
}
template <typename N>
int Earcut<N>::sign(double val) {
return (0.0 < val) - (val < 0.0);
}
// check if a polygon diagonal intersects any polygon segments
template <typename N>
bool Earcut<N>::intersectsPolygon(const Node* a, const Node* b) {
const Node* p = a;
do {
if (p->i != a->i && p->next->i != a->i && p->i != b->i && p->next->i != b->i &&
intersects(p, p->next, a, b)) return true;
p = p->next;
} while (p != a);
return false;
}
// check if a polygon diagonal is locally inside the polygon
template <typename N>
bool Earcut<N>::locallyInside(const Node* a, const Node* b) {
return area(a->prev, a, a->next) < 0 ?
area(a, b, a->next) >= 0 && area(a, a->prev, b) >= 0 :
area(a, b, a->prev) < 0 || area(a, a->next, b) < 0;
}
// check if the middle Vertex of a polygon diagonal is inside the polygon
template <typename N>
bool Earcut<N>::middleInside(const Node* a, const Node* b) {
const Node* p = a;
bool inside = false;
double px = (a->x + b->x) / 2;
double py = (a->y + b->y) / 2;
do {
if (((p->y > py) != (p->next->y > py)) && p->next->y != p->y &&
(px < (p->next->x - p->x) * (py - p->y) / (p->next->y - p->y) + p->x))
inside = !inside;
p = p->next;
} while (p != a);
return inside;
}
// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits
// polygon into two; if one belongs to the outer ring and another to a hole, it merges it into a
// single ring
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::splitPolygon(Node* a, Node* b) {
Node* a2 = nodes.construct(a->i, a->x, a->y);
Node* b2 = nodes.construct(b->i, b->x, b->y);
Node* an = a->next;
Node* bp = b->prev;
a->next = b;
b->prev = a;
a2->next = an;
an->prev = a2;
b2->next = a2;
a2->prev = b2;
bp->next = b2;
b2->prev = bp;
return b2;
}
// create a node and util::optionally link it with previous one (in a circular doubly linked list)
template <typename N> template <typename Point>
typename Earcut<N>::Node*
Earcut<N>::insertNode(std::size_t i, const Point& pt, Node* last) {
Node* p = nodes.construct(static_cast<N>(i), util::nth<0, Point>::get(pt), util::nth<1, Point>::get(pt));
if (!last) {
p->prev = p;
p->next = p;
} else {
assert(last);
p->next = last->next;
p->prev = last;
last->next->prev = p;
last->next = p;
}
return p;
}
template <typename N>
void Earcut<N>::removeNode(Node* p) {
p->next->prev = p->prev;
p->prev->next = p->next;
if (p->prevZ) p->prevZ->nextZ = p->nextZ;
if (p->nextZ) p->nextZ->prevZ = p->prevZ;
}
}
template <typename N = uint32_t, typename Polygon>
std::vector<N> earcut(const Polygon& poly) {
mapbox::detail::Earcut<N> earcut;
earcut(poly);
return std::move(earcut.indices);
}
}

View File

@ -1,43 +0,0 @@
#ifndef LOTTIE_MESH_BINDING_H
#define LOTTIE_MESH_BINDING_H
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NS_CLOSED_ENUM(NSInteger, LottieMeshFillRule) {
LottieMeshFillRuleEvenOdd,
LottieMeshFillRuleNonZero
};
@interface LottieMeshFill : NSObject
@property (nonatomic, readonly) LottieMeshFillRule fillRule;
- (instancetype _Nonnull)initWithFillRule:(LottieMeshFillRule)fillRule;
@end
@interface LottieMeshStroke : NSObject
@property (nonatomic, readonly) CGFloat lineWidth;
@property (nonatomic, readonly) CGLineJoin lineJoin;
@property (nonatomic, readonly) CGLineCap lineCap;
@property (nonatomic, readonly) CGFloat miterLimit;
- (instancetype _Nonnull)initWithLineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit;
@end
@interface LottieMeshData : NSObject
- (NSInteger)vertexCount;
- (void)getVertexAt:(NSInteger)index x:(float * _Nullable)x y:(float * _Nullable)y;
- (NSInteger)triangleCount;
- (void * _Nonnull)getTriangles;
+ (LottieMeshData * _Nullable)generateWithPath:(UIBezierPath * _Nonnull)path fill:(LottieMeshFill * _Nullable)fill stroke:(LottieMeshStroke * _Nullable)stroke;
@end
#endif

View File

@ -1,315 +0,0 @@
#import <LottieMeshBinding/LottieMeshBinding.h>
#import <LottieMesh/LottieMesh.h>
namespace {
MeshGenerator::Point bezierQuadraticPointAt(MeshGenerator::Point const &p0, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2, float t) {
float x = powf((1.0 - t), 2.0) * p0.x + 2.0 * (1.0 - t) * t * p1.x + powf(t, 2.0) * p2.x;
float y = powf((1.0 - t), 2.0) * p0.y + 2.0 * (1.0 - t) * t * p1.y + powf(t, 2.0) * p2.y;
return MeshGenerator::Point(x, y);
}
float approximateBezierQuadraticLength(MeshGenerator::Point const &p0, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2) {
float length = 0.0f;
float t = 0.1;
MeshGenerator::Point last = p0;
while (t < 1.01) {
auto point = bezierQuadraticPointAt(p0, p1, p2, t);
length += last.distance(point);
last = point;
t += 0.1;
}
return length;
}
void tesselateBezier(MeshGenerator::Path &path, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2, MeshGenerator::Point const &p3, MeshGenerator::Point const &p4, int level) {
const float tessTol = 0.25f / 0.1f;
float x1 = p1.x;
float y1 = p1.y;
float x2 = p2.x;
float y2 = p2.y;
float x3 = p3.x;
float y3 = p3.y;
float x4 = p4.x;
float y4 = p4.y;
float x12, y12, x23, y23, x34, y34, x123, y123, x234, y234, x1234, y1234;
float dx, dy, d2, d3;
if (level > 10) {
return;
}
x12 = (x1 + x2) * 0.5f;
y12 = (y1 + y2) * 0.5f;
x23 = (x2 + x3) * 0.5f;
y23 = (y2 + y3) * 0.5f;
x34 = (x3 + x4) * 0.5f;
y34 = (y3 + y4) * 0.5f;
x123 = (x12 + x23) * 0.5f;
y123 = (y12 + y23) * 0.5f;
dx = x4 - x1;
dy = y4 - y1;
d2 = std::abs(((x2 - x4) * dy - (y2 - y4) * dx));
d3 = std::abs(((x3 - x4) * dy - (y3 - y4) * dx));
if ((d2 + d3) * (d2 + d3) < tessTol * (dx * dx + dy * dy)) {
path.points.emplace_back(x4, y4);
return;
}
x234 = (x23+x34) * 0.5f;
y234 = (y23+y34) * 0.5f;
x1234 = (x123 + x234) * 0.5f;
y1234 = (y123 + y234) * 0.5f;
tesselateBezier(path, MeshGenerator::Point(x1, y1), MeshGenerator::Point(x12, y12), MeshGenerator::Point(x123, y123), MeshGenerator::Point(x1234, y1234), level + 1);
tesselateBezier(path, MeshGenerator::Point(x1234, y1234), MeshGenerator::Point(x234, y234), MeshGenerator::Point(x34, y34), MeshGenerator::Point(x4, y4), level + 1);
}
}
@interface LottieMeshData () {
std::unique_ptr<MeshGenerator::Mesh> _mesh;
}
- (instancetype _Nonnull)initWithMesh:(std::unique_ptr<MeshGenerator::Mesh> &&)mesh;
@end
@implementation LottieMeshData
- (instancetype _Nonnull)initWithMesh:(std::unique_ptr<MeshGenerator::Mesh> &&)mesh {
self = [super init];
if (self != nil) {
_mesh = std::move(mesh);
}
return self;
}
- (NSInteger)vertexCount {
return (NSInteger)_mesh->vertices.size();
}
- (void)getVertexAt:(NSInteger)index x:(float * _Nullable)x y:(float * _Nullable)y {
MeshGenerator::Point const &point = _mesh->vertices[index];
if (x) {
*x = point.x;
}
if (y) {
*y = point.y;
}
}
- (NSInteger)triangleCount {
return (NSInteger)(_mesh->triangles.size() / 3);
}
- (void * _Nonnull)getTriangles {
return _mesh->triangles.data();
}
/*- (void)getTriangleAt:(NSInteger)index v0:(NSInteger * _Nullable)v0 v1:(NSInteger * _Nullable)v1 v2:(NSInteger * _Nullable)v2 {
if (v0) {
*v0 = (NSInteger)_mesh->triangles[index * 3 + 0];
}
if (v1) {
*v1 = (NSInteger)_mesh->triangles[index * 3 + 1];
}
if (v2) {
*v2 = (NSInteger)_mesh->triangles[index * 3 + 2];
}
}*/
+ (LottieMeshData * _Nullable)generateWithPath:(UIBezierPath * _Nonnull)path fill: (LottieMeshFill * _Nullable)fill stroke:(LottieMeshStroke * _Nullable)stroke {
float scale = 1.0f;
float flatness = 1.0;
__block MeshGenerator::Point startingPoint(0.0f, 0.0f);
__block bool hasStartingPoint = false;
__block std::vector<MeshGenerator::Path> paths;
paths.push_back(MeshGenerator::Path());
CGPathApplyWithBlock(path.CGPath, ^(const CGPathElement * _Nonnull element) {
switch (element->type) {
case kCGPathElementMoveToPoint: {
if (!paths[paths.size() - 1].points.empty()) {
if (!paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
}
paths.push_back(MeshGenerator::Path());
}
startingPoint = MeshGenerator::Point((float)(element->points[0].x) * scale, (float)(element->points[0].y) * scale);
hasStartingPoint = true;
break;
}
case kCGPathElementAddLineToPoint: {
bool canAddPoints = false;
if (paths[paths.size() - 1].points.empty()) {
if (hasStartingPoint) {
paths[paths.size() - 1].points.push_back(startingPoint);
canAddPoints = true;
}
} else {
canAddPoints = true;
}
if (canAddPoints) {
paths[paths.size() - 1].points.push_back(MeshGenerator::Point((float)(element->points[0].x) * scale, (float)(element->points[0].y) * scale));
}
break;
}
case kCGPathElementAddQuadCurveToPoint: {
bool canAddPoints = false;
if (paths[paths.size() - 1].points.empty()) {
if (hasStartingPoint) {
paths[paths.size() - 1].points.push_back(startingPoint);
canAddPoints = true;
}
} else {
canAddPoints = true;
}
if (canAddPoints) {
float t = 0.001f;
MeshGenerator::Point p0 = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
MeshGenerator::Point p1(element->points[0].x * scale, element->points[0].y * scale);
MeshGenerator::Point p2(element->points[1].x * scale, element->points[1].y * scale);
float step = 10.0f * flatness / approximateBezierQuadraticLength(p0, p1, p2);
while (t < 1.0f) {
auto point = bezierQuadraticPointAt(p0, p1, p2, t);
paths[paths.size() - 1].points.push_back(point);
t += step;
}
paths[paths.size() - 1].points.push_back(p2);
}
break;
}
case kCGPathElementAddCurveToPoint: {
bool canAddPoints = false;
if (paths[paths.size() - 1].points.empty()) {
if (hasStartingPoint) {
paths[paths.size() - 1].points.push_back(startingPoint);
canAddPoints = true;
}
} else {
canAddPoints = true;
}
if (canAddPoints) {
float t = 0.001f;
MeshGenerator::Point p0 = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
MeshGenerator::Point p1(element->points[0].x * scale, element->points[0].y * scale);
MeshGenerator::Point p2(element->points[1].x * scale, element->points[1].y * scale);
MeshGenerator::Point p3(element->points[2].x * scale, element->points[2].y * scale);
tesselateBezier(paths[paths.size() - 1], p0, p1, p2, p3, 0);
}
break;
}
case kCGPathElementCloseSubpath: {
if (!paths[paths.size() - 1].points.empty()) {
if (!paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
}
hasStartingPoint = true;
startingPoint = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
paths.push_back(MeshGenerator::Path());
}
}
default: {
break;
}
}
});
if (!paths[paths.size() - 1].points.empty()) {
if (stroke == nil && !paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
}
} else {
paths.pop_back();
}
std::unique_ptr<MeshGenerator::Fill> mappedFill;
if (fill) {
mappedFill = std::make_unique<MeshGenerator::Fill>(fill.fillRule == LottieMeshFillRuleEvenOdd ? MeshGenerator::Fill::Rule::EvenOdd : MeshGenerator::Fill::Rule::NonZero);
}
std::unique_ptr<MeshGenerator::Stroke> mappedStroke;
if (stroke) {
MeshGenerator::Stroke::LineJoin lineJoin;
switch (stroke.lineJoin) {
case kCGLineJoinRound:
lineJoin = MeshGenerator::Stroke::LineJoin::Round;
break;
case kCGLineJoinBevel:
lineJoin = MeshGenerator::Stroke::LineJoin::Bevel;
break;
case kCGLineJoinMiter:
lineJoin = MeshGenerator::Stroke::LineJoin::Miter;
break;
default:
lineJoin = MeshGenerator::Stroke::LineJoin::Round;
break;
}
MeshGenerator::Stroke::LineCap lineCap;
switch (stroke.lineCap) {
case kCGLineCapRound:
lineCap = MeshGenerator::Stroke::LineCap::Round;
break;
case kCGLineCapButt:
lineCap = MeshGenerator::Stroke::LineCap::Butt;
break;
case kCGLineCapSquare:
lineCap = MeshGenerator::Stroke::LineCap::Square;
break;
default:
lineCap = MeshGenerator::Stroke::LineCap::Round;
break;
}
mappedStroke = std::make_unique<MeshGenerator::Stroke>((float)stroke.lineWidth, lineJoin, lineCap, (float)stroke.miterLimit);
}
std::unique_ptr<MeshGenerator::Mesh> resultMesh = MeshGenerator::generateMesh(paths, std::move(mappedFill), std::move(mappedStroke));
if (resultMesh) {
return [[LottieMeshData alloc] initWithMesh:std::move(resultMesh)];
} else {
return nil;
}
}
@end
@implementation LottieMeshFill
- (instancetype _Nonnull)initWithFillRule:(LottieMeshFillRule)fillRule {
self = [super init];
if (self != nil) {
_fillRule = fillRule;
}
return self;
}
@end
@implementation LottieMeshStroke
- (instancetype _Nonnull)initWithLineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit {
self = [super init];
if (self != nil) {
_lineWidth = lineWidth;
_lineJoin = lineJoin;
_lineCap = lineCap;
_miterLimit = miterLimit;
}
return self;
}
@end

View File

@ -1,82 +0,0 @@
#include <metal_stdlib>
using namespace metal;
typedef struct {
packed_float2 position;
} Vertex;
typedef struct {
packed_float2 offset;
} Offset;
typedef struct {
float4 position[[position]];
float2 localPosition[[center_no_perspective]];
} Varyings;
float2 screenSpaceToRelative(float2 point, float2 viewSize) {
float2 inverseViewSize = 1 / viewSize;
float clipX = (2.0f * point.x * inverseViewSize.x) - 2.0f;
float clipY = (2.0f * -point.y * inverseViewSize.y) + 2.0f;
return float2(clipX, clipY);
}
vertex Varyings vertexPassthrough(
constant Vertex *verticies[[buffer(0)]],
constant float2 &offset[[buffer(1)]],
unsigned int vid[[vertex_id]],
constant float4x4 &transformMatrix[[buffer(2)]],
constant int &indexOffset[[buffer(3)]]
) {
Varyings out;
constant Vertex &v = verticies[vid + indexOffset];
float2 viewSize(512.0f, 512.0f);
float4 transformedVertex = transformMatrix * float4(v.position, 0.0, 1.0);
out.position = float4(screenSpaceToRelative(float2(transformedVertex.x, transformedVertex.y) + offset, viewSize), 0.0, 1.0);
out.localPosition = float2(v.position);
return out;
}
fragment half4 fragmentPassthrough(
Varyings in[[stage_in]],
constant float4 &color[[buffer(1)]]
) {
float4 out = color;
return half4(out);
}
template<int N>
half4 mixGradientColors(float dist, constant float4 *colors, constant float *steps) {
float4 color = colors[0];
for (int i = 1; i < N; i++) {
color = mix(color, colors[i], smoothstep(steps[i - 1], steps[i], dist));
}
return half4(color);
}
#define radialGradientFunc(N) fragment half4 fragmentRadialGradient##N( \
Varyings in[[stage_in]], \
constant float2 &start[[buffer(1)]], \
constant float2 &end[[buffer(2)]], \
constant float4 *colors[[buffer(3)]], \
constant float *steps[[buffer(4)]] \
) { \
float centerDistance = distance(in.localPosition, start); \
float endDistance = distance(start, end); \
float dist = min(1.0, centerDistance / endDistance); \
return mixGradientColors<N>(dist, colors, steps); \
}
radialGradientFunc(2)
radialGradientFunc(3)
radialGradientFunc(4)
radialGradientFunc(5)
radialGradientFunc(6)
radialGradientFunc(7)
radialGradientFunc(8)
radialGradientFunc(9)
radialGradientFunc(10)

View File

@ -1,122 +0,0 @@
import Foundation
import Postbox
import ManagedFile
private let emptyMemory = malloc(1)!
public class MeshMemoryBuffer {
public internal(set) var data: Data
public internal(set) var length: Int
public init(data: Data) {
self.data = data
self.length = data.count
}
public func makeData() -> Data {
if self.data.count == self.length {
return self.data
} else {
return self.data.subdata(in: 0 ..< self.length)
}
}
}
extension WriteBuffer {
func writeInt32(_ value: Int32) {
var value = value
self.write(&value, length: 4)
}
func writeFloat(_ value: Float) {
var value: Float32 = value
self.write(&value, length: 4)
}
}
public final class MeshWriteBuffer {
let file: ManagedFile
private(set) var offset: Int = 0
public init(file: ManagedFile) {
self.file = file
}
public func write(_ data: UnsafeRawPointer, length: Int) {
let _ = self.file.write(data, count: length)
self.offset += length
}
public func writeInt8(_ value: Int8) {
var value = value
self.write(&value, length: 1)
}
public func writeInt32(_ value: Int32) {
var value = value
self.write(&value, length: 4)
}
public func writeFloat(_ value: Float) {
var value: Float32 = value
self.write(&value, length: 4)
}
public func write(_ data: Data) {
data.withUnsafeBytes { bytes in
self.write(bytes.baseAddress!, length: bytes.count)
}
}
func write(_ data: DataRange) {
data.data.withUnsafeBytes { bytes in
self.write(bytes.baseAddress!.advanced(by: data.range.lowerBound), length: data.count)
}
}
public func seek(offset: Int) {
let _ = self.file.seek(position: Int64(offset))
self.offset = offset
}
}
public final class MeshReadBuffer: MeshMemoryBuffer {
public var offset = 0
override public init(data: Data) {
super.init(data: data)
}
public func read(_ data: UnsafeMutableRawPointer, length: Int) {
self.data.copyBytes(to: data.assumingMemoryBound(to: UInt8.self), from: self.offset ..< (self.offset + length))
self.offset += length
}
func readDataRange(count: Int) -> DataRange {
let result = DataRange(data: self.data, range: self.offset ..< (self.offset + count))
self.offset += count
return result
}
public func readInt8() -> Int8 {
var result: Int8 = 0
self.read(&result, length: 1)
return result
}
public func readInt32() -> Int32 {
var result: Int32 = 0
self.read(&result, length: 4)
return result
}
public func readFloat() -> Float {
var result: Float32 = 0
self.read(&result, length: 4)
return result
}
public func skip(_ length: Int) {
self.offset += length
}
}

View File

@ -1,62 +0,0 @@
import Foundation
import UIKit
final class CapturedGeometryNode {
final class DisplayItem {
enum Display {
enum Style {
enum GradientType {
case linear
case radial
}
case color(color: UIColor, alpha: CGFloat)
case gradient(colors: [UIColor], positions: [CGFloat], start: CGPoint, end: CGPoint, type: GradientType)
}
struct Fill {
var style: Style
var fillRule: CGPathFillRule
}
struct Stroke {
var style: Style
var lineWidth: CGFloat
var lineCap: CGLineCap
var lineJoin: CGLineJoin
var miterLimit: CGFloat
}
case fill(Fill)
case stroke(Stroke)
}
let path: CGPath
let display: Display
init(path: CGPath, display: Display) {
self.path = path
self.display = display
}
}
var transform: CATransform3D
let alpha: CGFloat
let isHidden: Bool
let displayItem: DisplayItem?
let subnodes: [CapturedGeometryNode]
init(
transform: CATransform3D,
alpha: CGFloat,
isHidden: Bool,
displayItem: DisplayItem?,
subnodes: [CapturedGeometryNode]
) {
self.transform = transform
self.alpha = alpha
self.isHidden = isHidden
self.displayItem = displayItem
self.subnodes = subnodes
}
}

View File

@ -1,111 +0,0 @@
import Foundation
import QuartzCore
/**
The base class for a child layer of CompositionContainer
*/
class MyCompositionLayer {
var bounds: CGRect = CGRect()
let transformNode: LayerTransformNode
//let contentsLayer: CALayer = CALayer()
let maskLayer: MyMaskContainerLayer?
let matteType: MatteType?
var matteLayer: MyCompositionLayer? {
didSet {
//NOTE
/*if let matte = matteLayer {
if let type = matteType, type == .invert {
mask = InvertedMatteLayer(inputMatte: matte)
} else {
mask = matte
}
} else {
mask = nil
}*/
}
}
let inFrame: CGFloat
let outFrame: CGFloat
let startFrame: CGFloat
let timeStretch: CGFloat
init(layer: LayerModel, size: CGSize) {
self.transformNode = LayerTransformNode(transform: layer.transform)
if let masks = layer.masks {
maskLayer = MyMaskContainerLayer(masks: masks)
} else {
maskLayer = nil
}
self.matteType = layer.matte
self.inFrame = layer.inFrame.cgFloat
self.outFrame = layer.outFrame.cgFloat
self.timeStretch = layer.timeStretch.cgFloat
self.startFrame = layer.startTime.cgFloat
//NOTE
//self.anchorPoint = .zero
//NOTE
/*contentsLayer.anchorPoint = .zero
contentsLayer.bounds = CGRect(origin: .zero, size: size)
contentsLayer.actions = [
"opacity" : NSNull(),
"transform" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"sublayerTransform" : NSNull(),
"hidden" : NSNull()
]
addSublayer(contentsLayer)
if let maskLayer = maskLayer {
contentsLayer.mask = maskLayer
}*/
}
private(set) var isHidden = false
final func displayWithFrame(frame: CGFloat, forceUpdates: Bool) {
transformNode.updateTree(frame, forceUpdates: forceUpdates)
let layerVisible = frame.isInRangeOrEqual(inFrame, outFrame)
/// Only update contents if current time is within the layers time bounds.
if layerVisible {
displayContentsWithFrame(frame: frame, forceUpdates: forceUpdates)
maskLayer?.updateWithFrame(frame: frame, forceUpdates: forceUpdates)
}
self.isHidden = !layerVisible
//NOTE
/*contentsLayer.transform = transformNode.globalTransform
contentsLayer.opacity = transformNode.opacity
contentsLayer.isHidden = !layerVisible*/
}
func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
/// To be overridden by subclass
}
func captureGeometry() -> CapturedGeometryNode {
return CapturedGeometryNode(
transform: self.transformNode.globalTransform,
alpha: CGFloat(self.transformNode.opacity),
isHidden: self.isHidden,
displayItem: self.captureDisplayItem(),
subnodes: self.captureChildren()
)
}
func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
preconditionFailure()
}
func captureChildren() -> [CapturedGeometryNode] {
preconditionFailure()
}
}

View File

@ -1,36 +0,0 @@
import Foundation
import CoreGraphics
import QuartzCore
final class MyImageCompositionLayer: MyCompositionLayer {
var image: CGImage? = nil {
didSet {
//NOTE
/*if let image = image {
contentsLayer.contents = image
} else {
contentsLayer.contents = nil
}*/
}
}
let imageReferenceID: String
init(imageLayer: ImageLayerModel, size: CGSize) {
self.imageReferenceID = imageLayer.referenceID
super.init(layer: imageLayer, size: size)
//NOTE
//contentsLayer.masksToBounds = true
//contentsLayer.contentsGravity = CALayerContentsGravity.resize
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
preconditionFailure()
}
override func captureChildren() -> [CapturedGeometryNode] {
return []
}
}

View File

@ -1,150 +0,0 @@
import Foundation
import QuartzCore
extension MaskMode {
var usableMode: MaskMode {
switch self {
case .add:
return .add
case .subtract:
return .subtract
case .intersect:
return .intersect
case .lighten:
return .add
case .darken:
return .darken
case .difference:
return .intersect
case .none:
return .none
}
}
}
extension CGRect {
static var veryLargeRect: CGRect {
return CGRect(x: -100_000_000,
y: -100_000_000,
width: 200_000_000,
height: 200_000_000)
}
}
final class MyMaskContainerLayer {
init(masks: [Mask]) {
//NOTE
//anchorPoint = .zero
var containerLayer = CALayer()
var firstObject: Bool = true
for mask in masks {
let maskLayer = MaskLayer(mask: mask)
maskLayers.append(maskLayer)
if mask.mode.usableMode == .none {
continue
} else if mask.mode.usableMode == .add || firstObject {
firstObject = false
containerLayer.addSublayer(maskLayer)
} else {
containerLayer.mask = maskLayer
let newContainer = CALayer()
newContainer.addSublayer(containerLayer)
containerLayer = newContainer
}
}
//NOTE
//addSublayer(containerLayer)
}
fileprivate var maskLayers: [MaskLayer] = []
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
maskLayers.forEach({ $0.updateWithFrame(frame: frame, forceUpdates: forceUpdates) })
}
}
fileprivate class MaskLayer: CALayer {
let properties: MaskNodeProperties?
let maskLayer = CAShapeLayer()
init(mask: Mask) {
self.properties = MaskNodeProperties(mask: mask)
super.init()
addSublayer(maskLayer)
anchorPoint = .zero
maskLayer.fillColor = mask.mode == .add ? CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 1]) :
CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 1])
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
self.actions = [
"opacity" : NSNull()
]
}
override init(layer: Any) {
self.properties = nil
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
guard let properties = properties else { return }
if properties.opacity.needsUpdate(frame: frame) || forceUpdates {
properties.opacity.update(frame: frame)
self.opacity = Float(properties.opacity.value.cgFloatValue)
}
if properties.shape.needsUpdate(frame: frame) || forceUpdates {
properties.shape.update(frame: frame)
properties.expansion.update(frame: frame)
let shapePath = properties.shape.value.cgPath()
var path = shapePath
if properties.mode.usableMode == .subtract && !properties.inverted ||
(properties.mode.usableMode == .add && properties.inverted) {
/// Add a bounds rect to invert the mask
let newPath = CGMutablePath()
newPath.addRect(CGRect.veryLargeRect)
newPath.addPath(shapePath)
path = newPath
}
maskLayer.path = path
}
}
}
fileprivate class MaskNodeProperties: NodePropertyMap {
var propertyMap: [String : AnyNodeProperty]
var properties: [AnyNodeProperty]
init(mask: Mask) {
self.mode = mask.mode
self.inverted = mask.inverted
self.opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.opacity.keyframes))
self.shape = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.shape.keyframes))
self.expansion = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.expansion.keyframes))
self.propertyMap = [
"Opacity" : opacity,
"Shape" : shape,
"Expansion" : expansion
]
self.properties = Array(self.propertyMap.values)
}
let mode: MaskMode
let inverted: Bool
let opacity: NodeProperty<Vector1D>
let shape: NodeProperty<BezierPath>
let expansion: NodeProperty<Vector1D>
}

View File

@ -1,15 +0,0 @@
import Foundation
final class MyNullCompositionLayer: MyCompositionLayer {
init(layer: LayerModel) {
super.init(layer: layer, size: .zero)
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
return nil
}
override func captureChildren() -> [CapturedGeometryNode] {
return []
}
}

View File

@ -1,84 +0,0 @@
import Foundation
import QuartzCore
final class MyPreCompositionLayer: MyCompositionLayer {
let frameRate: CGFloat
let remappingNode: NodeProperty<Vector1D>?
fileprivate var animationLayers: [MyCompositionLayer]
init(precomp: PreCompLayerModel,
asset: PrecompAsset,
assetLibrary: AssetLibrary?,
frameRate: CGFloat) {
self.animationLayers = []
if let keyframes = precomp.timeRemapping?.keyframes {
self.remappingNode = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframes))
} else {
self.remappingNode = nil
}
self.frameRate = frameRate
super.init(layer: precomp, size: CGSize(width: precomp.width, height: precomp.height))
bounds = CGRect(origin: .zero, size: CGSize(width: precomp.width, height: precomp.height))
//NOTE
//contentsLayer.masksToBounds = true
//contentsLayer.bounds = bounds
let layers = initializeCompositionLayers(layers: asset.layers, assetLibrary: assetLibrary, frameRate: frameRate)
var imageLayers = [MyImageCompositionLayer]()
var mattedLayer: MyCompositionLayer? = nil
for layer in layers.reversed() {
layer.bounds = bounds
//NOTE
animationLayers.append(layer)
if let imageLayer = layer as? MyImageCompositionLayer {
imageLayers.append(imageLayer)
}
if let matte = mattedLayer {
/// The previous layer requires this layer to be its matte
matte.matteLayer = layer
mattedLayer = nil
continue
}
if let matte = layer.matteType,
(matte == .add || matte == .invert) {
/// We have a layer that requires a matte.
mattedLayer = layer
}
//NOTE
//contentsLayer.addSublayer(layer)
}
//NOTE
//layerImageProvider.addImageLayers(imageLayers)
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
let localFrame: CGFloat
if let remappingNode = remappingNode {
remappingNode.update(frame: frame)
localFrame = remappingNode.value.cgFloatValue * frameRate
} else {
localFrame = (frame - startFrame) / timeStretch
}
for animationLayer in self.animationLayers {
animationLayer.displayWithFrame(frame: localFrame, forceUpdates: forceUpdates)
}
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
return nil
}
override func captureChildren() -> [CapturedGeometryNode] {
var result: [CapturedGeometryNode] = []
for animationLayer in self.animationLayers {
result.append(animationLayer.captureGeometry())
}
return result
}
}

View File

@ -1,54 +0,0 @@
import Foundation
import CoreGraphics
/**
A CompositionLayer responsible for initializing and rendering shapes
*/
final class MyShapeCompositionLayer: MyCompositionLayer {
let rootNode: AnimatorNode?
let renderContainer: ShapeContainerLayer?
init(shapeLayer: ShapeLayerModel) {
let results = shapeLayer.items.initializeNodeTree()
let renderContainer = ShapeContainerLayer()
self.renderContainer = renderContainer
self.rootNode = results.rootNode
super.init(layer: shapeLayer, size: .zero)
//NOTE
//contentsLayer.addSublayer(renderContainer)
for container in results.renderContainers {
renderContainer.insertRenderLayer(container)
}
rootNode?.updateTree(0, forceUpdates: true)
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
rootNode?.updateTree(frame, forceUpdates: forceUpdates)
renderContainer?.markRenderUpdates(forFrame: frame)
}
override func captureGeometry() -> CapturedGeometryNode {
var subnodes: [CapturedGeometryNode] = []
if let renderContainer = self.renderContainer {
subnodes.append(renderContainer.captureGeometry())
}
return CapturedGeometryNode(
transform: self.transformNode.globalTransform,
alpha: CGFloat(self.transformNode.opacity),
isHidden: self.isHidden,
displayItem: nil,
subnodes: subnodes
)
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
preconditionFailure()
}
override func captureChildren() -> [CapturedGeometryNode] {
preconditionFailure()
}
}

View File

@ -1,41 +0,0 @@
import Foundation
import UIKit
final class MySolidCompositionLayer: MyCompositionLayer {
let colorProperty: NodeProperty<Color>?
let solidShape: CAShapeLayer = CAShapeLayer()
init(solid: SolidLayerModel) {
let components = solid.colorHex.hexColorComponents()
self.colorProperty = NodeProperty(provider: SingleValueProvider(Color(r: Double(components.red), g: Double(components.green), b: Double(components.blue), a: 1)))
super.init(layer: solid, size: .zero)
solidShape.path = CGPath(rect: CGRect(x: 0, y: 0, width: solid.width, height: solid.height), transform: nil)
//NOTE
//contentsLayer.addSublayer(solidShape)
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
guard let colorProperty = colorProperty else { return }
colorProperty.update(frame: frame)
solidShape.fillColor = colorProperty.value.cgColorValue
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
guard let colorProperty = colorProperty else { return nil }
guard let path = self.solidShape.path else {
return nil
}
return CapturedGeometryNode.DisplayItem(
path: path,
display: .fill(CapturedGeometryNode.DisplayItem.Display.Fill(
style: .color(color: UIColor(cgColor: colorProperty.value.cgColorValue), alpha: 1.0),
fillRule: .evenOdd
))
)
}
override func captureChildren() -> [CapturedGeometryNode] {
return []
}
}

View File

@ -1,58 +0,0 @@
//
// InvertedMatteLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/28/19.
//
import Foundation
import QuartzCore
/**
A layer that inverses the alpha output of its input layer.
WARNING: This is experimental and probably not very performant.
*/
/*final class InvertedMatteLayer: CALayer, CompositionLayerDelegate {
let inputMatte: CompositionLayer?
let wrapperLayer = CALayer()
init(inputMatte: CompositionLayer) {
self.inputMatte = inputMatte
super.init()
inputMatte.layerDelegate = self
self.anchorPoint = .zero
self.bounds = inputMatte.bounds
self.setNeedsDisplay()
}
override init(layer: Any) {
guard let layer = layer as? InvertedMatteLayer else {
fatalError("init(layer:) wrong class.")
}
self.inputMatte = nil
super.init(layer: layer)
}
func frameUpdated(frame: CGFloat) {
self.setNeedsDisplay()
self.displayIfNeeded()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(in ctx: CGContext) {
guard let inputMatte = inputMatte else { return }
guard let fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1])
else { return }
ctx.setFillColor(fillColor)
ctx.fill(bounds)
ctx.setBlendMode(.destinationOut)
inputMatte.render(in: ctx)
}
}
*/

View File

@ -1,120 +0,0 @@
//
// LayerTransformPropertyMap.swift
// lottie-swift
//
// Created by Brandon Withrow on 2/4/19.
//
import Foundation
import CoreGraphics
import QuartzCore
final class LayerTransformProperties: NodePropertyMap {
init(transform: Transform) {
self.anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes))
self.scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
self.rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
self.opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
var propertyMap: [String: AnyNodeProperty] = [
"Anchor Point" : anchor,
"Scale" : scale,
"Rotation" : rotation,
"Opacity" : opacity
]
if let positionKeyframesX = transform.positionX?.keyframes,
let positionKeyframesY = transform.positionY?.keyframes {
let xPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesX))
let yPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesY))
propertyMap["X Position"] = xPosition
propertyMap["Y Position"] = yPosition
self.positionX = xPosition
self.positionY = yPosition
self.position = nil
} else if let positionKeyframes = transform.position?.keyframes {
let position: NodeProperty<Vector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
propertyMap["Position"] = position
self.position = position
self.positionX = nil
self.positionY = nil
} else {
self.position = nil
self.positionY = nil
self.positionX = nil
}
self.properties = Array(propertyMap.values)
}
let properties: [AnyNodeProperty]
let anchor: NodeProperty<Vector3D>
let scale: NodeProperty<Vector3D>
let rotation: NodeProperty<Vector1D>
let position: NodeProperty<Vector3D>?
let positionX: NodeProperty<Vector1D>?
let positionY: NodeProperty<Vector1D>?
let opacity: NodeProperty<Vector1D>
}
class LayerTransformNode: AnimatorNode {
let outputNode: NodeOutput = PassThroughOutputNode(parent: nil)
init(transform: Transform) {
self.transformProperties = LayerTransformProperties(transform: transform)
}
let transformProperties: LayerTransformProperties
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap {
return transformProperties
}
var parentNode: AnimatorNode?
var hasLocalUpdates: Bool = false
var hasUpstreamUpdates: Bool = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled: Bool = true
func shouldRebuildOutputs(frame: CGFloat) -> Bool {
return hasLocalUpdates || hasUpstreamUpdates
}
func rebuildOutputs(frame: CGFloat) {
opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
let position: CGPoint
if let point = transformProperties.position?.value.pointValue {
position = point
} else if let xPos = transformProperties.positionX?.value.cgFloatValue,
let yPos = transformProperties.positionY?.value.cgFloatValue {
position = CGPoint(x: xPos, y: yPos)
} else {
position = .zero
}
localTransform = CATransform3D.makeTransform(anchor: transformProperties.anchor.value.pointValue,
position: position,
scale: transformProperties.scale.value.sizeValue,
rotation: transformProperties.rotation.value.cgFloatValue,
skew: nil,
skewAxis: nil)
if let parentNode = parentNode as? LayerTransformNode {
globalTransform = CATransform3DConcat(localTransform, parentNode.globalTransform)
} else {
globalTransform = localTransform
}
}
var opacity: Float = 1
var localTransform: CATransform3D = CATransform3DIdentity
var globalTransform: CATransform3D = CATransform3DIdentity
}

View File

@ -1,107 +0,0 @@
//
// Animation.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/7/19.
//
import Foundation
public enum CoordinateSpace: Int, Codable {
case type2d
case type3d
}
/**
The `Animation` model is the top level model object in Lottie.
An `Animation` holds all of the animation data backing a Lottie Animation.
Codable, see JSON schema [here](https://github.com/airbnb/lottie-web/tree/master/docs/json).
*/
public final class Animation: Codable {
/// The version of the JSON Schema.
let version: String
/// The coordinate space of the composition.
let type: CoordinateSpace
/// The start time of the composition in frameTime.
public let startFrame: AnimationFrameTime
/// The end time of the composition in frameTime.
public let endFrame: AnimationFrameTime
/// The frame rate of the composition.
public let framerate: Double
/// The height of the composition in points.
let width: Int
/// The width of the composition in points.
let height: Int
/// The list of animation layers
let layers: [LayerModel]
/// The list of glyphs used for text rendering
let glyphs: [Glyph]?
/// The list of fonts used for text rendering
let fonts: FontList?
/// Asset Library
let assetLibrary: AssetLibrary?
/// Markers
let markers: [Marker]?
let markerMap: [String : Marker]?
/// Return all marker names, in order, or an empty list if none are specified
public var markerNames: [String] {
guard let markers = markers else { return [] }
return markers.map { $0.name }
}
enum CodingKeys : String, CodingKey {
case version = "v"
case type = "ddd"
case startFrame = "ip"
case endFrame = "op"
case framerate = "fr"
case width = "w"
case height = "h"
case layers = "layers"
case glyphs = "chars"
case fonts = "fonts"
case assetLibrary = "assets"
case markers = "markers"
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Animation.CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.type = try container.decodeIfPresent(CoordinateSpace.self, forKey: .type) ?? .type2d
self.startFrame = try container.decode(AnimationFrameTime.self, forKey: .startFrame)
self.endFrame = try container.decode(AnimationFrameTime.self, forKey: .endFrame)
self.framerate = try container.decode(Double.self, forKey: .framerate)
self.width = try container.decode(Int.self, forKey: .width)
self.height = try container.decode(Int.self, forKey: .height)
self.layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
self.glyphs = try container.decodeIfPresent([Glyph].self, forKey: .glyphs)
self.fonts = try container.decodeIfPresent(FontList.self, forKey: .fonts)
self.assetLibrary = try container.decodeIfPresent(AssetLibrary.self, forKey: .assetLibrary)
self.markers = try container.decodeIfPresent([Marker].self, forKey: .markers)
if let markers = markers {
var markerMap: [String : Marker] = [:]
for marker in markers {
markerMap[marker.name] = marker
}
self.markerMap = markerMap
} else {
self.markerMap = nil
}
}
}

View File

@ -1,27 +0,0 @@
//
// Asset.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
public class Asset: Codable {
/// The ID of the asset
public let id: String
private enum CodingKeys : String, CodingKey {
case id = "id"
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Asset.CodingKeys.self)
if let id = try? container.decode(String.self, forKey: .id) {
self.id = id
} else {
self.id = String(try container.decode(Int.self, forKey: .id))
}
}
}

View File

@ -1,48 +0,0 @@
//
// AssetLibrary.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
final class AssetLibrary: Codable {
/// The Assets
let assets: [String : Asset]
let imageAssets: [String : ImageAsset]
let precompAssets: [String : PrecompAsset]
required init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var containerForKeys = container
var decodedAssets = [String : Asset]()
var imageAssets = [String : ImageAsset]()
var precompAssets = [String : PrecompAsset]()
while !container.isAtEnd {
let keyContainer = try containerForKeys.nestedContainer(keyedBy: PrecompAsset.CodingKeys.self)
if keyContainer.contains(.layers) {
let precompAsset = try container.decode(PrecompAsset.self)
decodedAssets[precompAsset.id] = precompAsset
precompAssets[precompAsset.id] = precompAsset
} else {
let imageAsset = try container.decode(ImageAsset.self)
decodedAssets[imageAsset.id] = imageAsset
imageAssets[imageAsset.id] = imageAsset
}
}
self.assets = decodedAssets
self.precompAssets = precompAssets
self.imageAssets = imageAssets
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(contentsOf: Array(assets.values))
}
}

View File

@ -1,48 +0,0 @@
//
// ImageAsset.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
public final class ImageAsset: Asset {
/// Image name
public let name: String
/// Image Directory
public let directory: String
/// Image Size
public let width: Double
public let height: Double
enum CodingKeys : String, CodingKey {
case name = "p"
case directory = "u"
case width = "w"
case height = "h"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ImageAsset.CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.directory = try container.decode(String.self, forKey: .directory)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(directory, forKey: .directory)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
}
}

View File

@ -1,30 +0,0 @@
//
// PrecompAsset.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
final class PrecompAsset: Asset {
/// Layers of the precomp
let layers: [LayerModel]
enum CodingKeys : String, CodingKey {
case layers = "layers"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PrecompAsset.CodingKeys.self)
self.layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(layers, forKey: .layers)
}
}

View File

@ -1,21 +0,0 @@
import Foundation
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
import UIKit
#endif
extension Bundle {
func getAnimationData(_ name: String, subdirectory: String? = nil) throws -> Data? {
// Check for files in the bundle at the given path
if let url = self.url(forResource: name, withExtension: "json", subdirectory: subdirectory) {
return try Data(contentsOf: url)
}
// Check for data assets (not available on macOS)
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
let assetKey = subdirectory != nil ? "\(subdirectory ?? "")/\(name)" : name
return NSDataAsset.init(name: assetKey, bundle: self)?.data
#else
return nil
#endif
}
}

View File

@ -1,40 +0,0 @@
// From: https://medium.com/@kewindannerfjordremeczki/swift-4-0-decodable-heterogeneous-collections-ecc0e6b468cf
import Foundation
/// To support a new class family, create an enum that conforms to this protocol and contains the different types.
protocol ClassFamily: Decodable {
/// The discriminator key.
static var discriminator: Discriminator { get }
/// Returns the class type of the object corresponding to the value.
func getType() -> AnyObject.Type
}
/// Discriminator key enum used to retrieve discriminator fields in JSON payloads.
enum Discriminator: String, CodingKey {
case type = "ty"
}
extension KeyedDecodingContainer {
/// Decode a heterogeneous list of objects for a given family.
/// - Parameters:
/// - heterogeneousType: The decodable type of the list.
/// - family: The ClassFamily enum for the type family.
/// - key: The CodingKey to look up the list in the current container.
/// - Returns: The resulting list of heterogeneousType elements.
func decode<T : Decodable, U : ClassFamily>(_ heterogeneousType: [T].Type, ofFamily family: U.Type, forKey key: K) throws -> [T] {
var container = try self.nestedUnkeyedContainer(forKey: key)
var list = [T]()
var tmpContainer = container
while !container.isAtEnd {
let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self)
let family: U = try typeContainer.decode(U.self, forKey: U.discriminator)
if let type = family.getType() as? T.Type {
list.append(try tmpContainer.decode(type))
}
}
return list
}
}

View File

@ -1,128 +0,0 @@
//
// Keyframe.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/7/19.
//
import Foundation
import CoreGraphics
/**
Keyframe represents a point in time and is the container for datatypes.
Note: This is a parent class and should not be used directly.
*/
final class Keyframe<T: Interpolatable> {
/// The value of the keyframe
let value: T
/// The time in frames of the keyframe.
let time: CGFloat
/// A hold keyframe freezes interpolation until the next keyframe that is not a hold.
let isHold: Bool
/// The in tangent for the time interpolation curve.
let inTangent: Vector2D?
/// The out tangent for the time interpolation curve.
let outTangent: Vector2D?
/// The spacial in tangent of the vector.
let spatialInTangent: Vector3D?
/// The spacial out tangent of the vector.
let spatialOutTangent: Vector3D?
/// Initialize a value-only keyframe with no time data.
init(_ value: T,
spatialInTangent: Vector3D? = nil,
spatialOutTangent: Vector3D? = nil) {
self.value = value
self.time = 0
self.isHold = true
self.inTangent = nil
self.outTangent = nil
self.spatialInTangent = spatialInTangent
self.spatialOutTangent = spatialOutTangent
}
/// Initialize a keyframe
init(value: T,
time: Double,
isHold: Bool,
inTangent: Vector2D?,
outTangent: Vector2D?,
spatialInTangent: Vector3D? = nil,
spatialOutTangent: Vector3D? = nil) {
self.value = value
self.time = CGFloat(time)
self.isHold = isHold
self.outTangent = outTangent
self.inTangent = inTangent
self.spatialInTangent = spatialInTangent
self.spatialOutTangent = spatialOutTangent
}
}
/**
A generic class used to parse and remap keyframe json.
Keyframe json has a couple of different variations and formats depending on the
type of keyframea and also the version of the JSON. By parsing the raw data
we can reconfigure it into a constant format.
*/
final class KeyframeData<T: Codable>: Codable {
/// The start value of the keyframe
let startValue: T?
/// The End value of the keyframe. Note: Newer versions animation json do not have this field.
let endValue: T?
/// The time in frames of the keyframe.
let time: Double?
/// A hold keyframe freezes interpolation until the next keyframe that is not a hold.
let hold: Int?
/// The in tangent for the time interpolation curve.
let inTangent: Vector2D?
/// The out tangent for the time interpolation curve.
let outTangent: Vector2D?
/// The spacial in tangent of the vector.
let spatialInTangent: Vector3D?
/// The spacial out tangent of the vector.
let spatialOutTangent:Vector3D?
init(startValue: T?,
endValue: T?,
time: Double?,
hold: Int?,
inTangent: Vector2D?,
outTangent: Vector2D?,
spatialInTangent: Vector3D?,
spatialOutTangent: Vector3D?) {
self.startValue = startValue
self.endValue = endValue
self.time = time
self.hold = hold
self.inTangent = inTangent
self.outTangent = outTangent
self.spatialInTangent = spatialInTangent
self.spatialOutTangent = spatialOutTangent
}
enum CodingKeys : String, CodingKey {
case startValue = "s"
case endValue = "e"
case time = "t"
case hold = "h"
case inTangent = "i"
case outTangent = "o"
case spatialInTangent = "ti"
case spatialOutTangent = "to"
}
var isHold: Bool {
if let hold = hold {
return hold > 0
}
return false
}
}

View File

@ -1,108 +0,0 @@
//
// KeyframeGroup.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/14/19.
//
import Foundation
/**
Used for coding/decoding a group of Keyframes by type.
Keyframe data is wrapped in a dictionary { "k" : KeyframeData }.
The keyframe data can either be an array of keyframes or, if no animation is present, the raw value.
This helper object is needed to properly decode the json.
*/
final class KeyframeGroup<T>: Codable where T: Codable, T: Interpolatable {
let keyframes: ContiguousArray<Keyframe<T>>
private enum KeyframeWrapperKey: String, CodingKey {
case keyframeData = "k"
}
init(keyframes: ContiguousArray<Keyframe<T>>) {
self.keyframes = keyframes
}
init(_ value: T) {
self.keyframes = [Keyframe(value)]
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: KeyframeWrapperKey.self)
if let keyframeData: T = try? container.decode(T.self, forKey: .keyframeData) {
/// Try to decode raw value; No keyframe data.
self.keyframes = [Keyframe<T>(keyframeData)]
} else {
/**
Decode and array of keyframes.
Body Movin and Lottie deal with keyframes in different ways.
A keyframe object in Body movin defines a span of time with a START
and an END, from the current keyframe time to the next keyframe time.
A keyframe object in Lottie defines a singular point in time/space.
This point has an in-tangent and an out-tangent.
To properly decode this we must iterate through keyframes while holding
reference to the previous keyframe.
*/
var keyframesContainer = try container.nestedUnkeyedContainer(forKey: .keyframeData)
var keyframes = ContiguousArray<Keyframe<T>>()
var previousKeyframeData: KeyframeData<T>?
while(!keyframesContainer.isAtEnd) {
// Ensure that Time and Value are present.
let keyframeData = try keyframesContainer.decode(KeyframeData<T>.self)
guard let value: T = keyframeData.startValue ?? previousKeyframeData?.endValue,
let time = keyframeData.time else {
/// Missing keyframe data. JSON must be corrupt.
throw DecodingError.dataCorruptedError(forKey: KeyframeWrapperKey.keyframeData, in: container, debugDescription: "Missing keyframe data.")
}
keyframes.append(Keyframe<T>(value: value,
time: time,
isHold: keyframeData.isHold,
inTangent: previousKeyframeData?.inTangent,
outTangent: keyframeData.outTangent,
spatialInTangent: previousKeyframeData?.spatialInTangent,
spatialOutTangent: keyframeData.spatialOutTangent))
previousKeyframeData = keyframeData
}
self.keyframes = keyframes
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: KeyframeWrapperKey.self)
if keyframes.count == 1 {
let keyframe = keyframes[0]
try container.encode(keyframe.value, forKey: .keyframeData)
} else {
var keyframeContainer = container.nestedUnkeyedContainer(forKey: .keyframeData)
for i in 1..<keyframes.endIndex {
let keyframe = keyframes[i-1]
let nextKeyframe = keyframes[i]
let keyframeData = KeyframeData<T>(startValue: keyframe.value,
endValue: nextKeyframe.value,
time: Double(keyframe.time),
hold: keyframe.isHold ? 1 : nil,
inTangent: nextKeyframe.inTangent,
outTangent: keyframe.outTangent,
spatialInTangent: nil,
spatialOutTangent: nil)
try keyframeContainer.encode(keyframeData)
}
}
}
}

View File

@ -1,32 +0,0 @@
//
// ImageLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds an image.
final class ImageLayerModel: LayerModel {
/// The reference ID of the image.
let referenceID: String
private enum CodingKeys : String, CodingKey {
case referenceID = "refId"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ImageLayerModel.CodingKeys.self)
self.referenceID = try container.decode(String.self, forKey: .referenceID)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(referenceID, forKey: .referenceID)
}
}

View File

@ -1,150 +0,0 @@
//
// Layer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/7/19.
//
import Foundation
/// Used for mapping a heterogeneous list to classes for parsing.
extension LayerType: ClassFamily {
static var discriminator: Discriminator = .type
func getType() -> AnyObject.Type {
switch self {
case .precomp:
return PreCompLayerModel.self
case .solid:
return SolidLayerModel.self
case .image:
return ImageLayerModel.self
case .null:
return LayerModel.self
case .shape:
return ShapeLayerModel.self
case .text:
return TextLayerModel.self
}
}
}
public enum LayerType: Int, Codable {
case precomp
case solid
case image
case null
case shape
case text
public init(from decoder: Decoder) throws {
self = try LayerType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .null
}
}
public enum MatteType: Int, Codable {
case none
case add
case invert
case unknown
}
public enum BlendMode: Int, Codable {
case normal
case multiply
case screen
case overlay
case darken
case lighten
case colorDodge
case colorBurn
case hardLight
case softLight
case difference
case exclusion
case hue
case saturation
case color
case luminosity
}
/**
A base top container for shapes, images, and other view objects.
*/
class LayerModel: Codable {
/// The readable name of the layer
let name: String
/// The index of the layer
let index: Int
/// The type of the layer.
let type: LayerType
/// The coordinate space
let coordinateSpace: CoordinateSpace
/// The in time of the layer in frames.
let inFrame: Double
/// The out time of the layer in frames.
let outFrame: Double
/// The start time of the layer in frames.
let startTime: Double
/// The transform of the layer
let transform: Transform
/// The index of the parent layer, if applicable.
let parent: Int?
/// The blending mode for the layer
let blendMode: BlendMode
/// An array of masks for the layer.
let masks: [Mask]?
/// A number that stretches time by a multiplier
let timeStretch: Double
/// The type of matte if any.
let matte: MatteType?
let hidden: Bool
private enum CodingKeys : String, CodingKey {
case name = "nm"
case index = "ind"
case type = "ty"
case coordinateSpace = "ddd"
case inFrame = "ip"
case outFrame = "op"
case startTime = "st"
case transform = "ks"
case parent = "parent"
case blendMode = "bm"
case masks = "masksProperties"
case timeStretch = "sr"
case matte = "tt"
case hidden = "hd"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: LayerModel.CodingKeys.self)
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Layer"
self.index = try container.decode(Int.self, forKey: .index)
self.type = try container.decode(LayerType.self, forKey: .type)
self.coordinateSpace = try container.decodeIfPresent(CoordinateSpace.self, forKey: .coordinateSpace) ?? .type2d
self.inFrame = try container.decode(Double.self, forKey: .inFrame)
self.outFrame = try container.decode(Double.self, forKey: .outFrame)
self.startTime = try container.decode(Double.self, forKey: .startTime)
self.transform = try container.decode(Transform.self, forKey: .transform)
self.parent = try container.decodeIfPresent(Int.self, forKey: .parent)
self.blendMode = try container.decodeIfPresent(BlendMode.self, forKey: .blendMode) ?? .normal
self.masks = try container.decodeIfPresent([Mask].self, forKey: .masks)
self.timeStretch = try container.decodeIfPresent(Double.self, forKey: .timeStretch) ?? 1
self.matte = try container.decodeIfPresent(MatteType.self, forKey: .matte)
self.hidden = try container.decodeIfPresent(Bool.self, forKey: .hidden) ?? false
}
}

View File

@ -1,50 +0,0 @@
//
// PreCompLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds another animation composition.
final class PreCompLayerModel: LayerModel {
/// The reference ID of the precomp.
let referenceID: String
/// A value that remaps time over time.
let timeRemapping: KeyframeGroup<Vector1D>?
/// Precomp Width
let width: Double
/// Precomp Height
let height: Double
private enum CodingKeys : String, CodingKey {
case referenceID = "refId"
case timeRemapping = "tm"
case width = "w"
case height = "h"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PreCompLayerModel.CodingKeys.self)
self.referenceID = try container.decode(String.self, forKey: .referenceID)
self.timeRemapping = try container.decodeIfPresent(KeyframeGroup<Vector1D>.self, forKey: .timeRemapping)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(referenceID, forKey: .referenceID)
try container.encode(timeRemapping, forKey: .timeRemapping)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
}
}

View File

@ -1,32 +0,0 @@
//
// ShapeLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds vector shape objects.
final class ShapeLayerModel: LayerModel {
/// A list of shape items.
let items: [ShapeItem]
private enum CodingKeys : String, CodingKey {
case items = "shapes"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ShapeLayerModel.CodingKeys.self)
self.items = try container.decode([ShapeItem].self, ofFamily: ShapeType.self, forKey: .items)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.items, forKey: .items)
}
}

View File

@ -1,44 +0,0 @@
//
// SolidLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds a solid color.
final class SolidLayerModel: LayerModel {
/// The color of the solid in Hex // Change to value provider.
let colorHex: String
/// The Width of the color layer
let width: Double
/// The height of the color layer
let height: Double
private enum CodingKeys : String, CodingKey {
case colorHex = "sc"
case width = "sw"
case height = "sh"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SolidLayerModel.CodingKeys.self)
self.colorHex = try container.decode(String.self, forKey: .colorHex)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(colorHex, forKey: .colorHex)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
}
}

View File

@ -1,44 +0,0 @@
//
// TextLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds text.
final class TextLayerModel: LayerModel {
/// The text for the layer
let text: KeyframeGroup<TextDocument>
/// Text animators
let animators: [TextAnimator]
private enum CodingKeys : String, CodingKey {
case textGroup = "t"
}
private enum TextCodingKeys : String, CodingKey {
case text = "d"
case animators = "a"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TextLayerModel.CodingKeys.self)
let textContainer = try container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup)
self.text = try textContainer.decode(KeyframeGroup<TextDocument>.self, forKey: .text)
self.animators = try textContainer.decode([TextAnimator].self, forKey: .animators)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
var textContainer = container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup)
try textContainer.encode(text, forKey: .text)
try textContainer.encode(animators, forKey: .animators)
}
}

View File

@ -1,24 +0,0 @@
//
// DashPattern.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
enum DashElementType: String, Codable {
case offset = "o"
case dash = "d"
case gap = "g"
}
final class DashElement: Codable {
let type: DashElementType
let value: KeyframeGroup<Vector1D>
enum CodingKeys : String, CodingKey {
case type = "n"
case value = "v"
}
}

Some files were not shown because too many files have changed in this diff Show More