Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-04-22 13:08:27 +04:00
commit 20910f7ca7
391 changed files with 3915 additions and 20278 deletions

View file

@ -9112,11 +9112,6 @@ Sorry for the inconvenience.";
"Wallpaper.ApplyForAll" = "Apply For All Chats";
"Wallpaper.ApplyForChat" = "Apply For This Chat";
"ChatList.ChatFolderUpdateCount_1" = "1 new chat";
"ChatList.ChatFolderUpdateCount_any" = "%d new chats";
"ChatList.ChatFolderUpdateHintTitle" = "You can join %@";
"ChatList.ChatFolderUpdateHintText" = "Tap here to view them";
"Premium.MaxSharedFolderMembershipText" = "You can only add **%1$@** shareable folders. Upgrade to **Telegram Premium** to increase this limit up to **%2$@**.";
"Premium.MaxSharedFolderMembershipNoPremiumText" = "You can only add **%1$@** shareable folders. We are working to let you increase this limit in the future.";
"Premium.MaxSharedFolderMembershipFinalText" = "Sorry, you can only add **%1$@** shareable folders.";

View file

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

View file

@ -10,7 +10,6 @@ import AsyncDisplayKit
import Display
import DeviceLocationManager
import TemporaryCachedPeerDataManager
import MeshAnimationCache
import InAppPurchaseManager
import AnimationCache
import MultiAnimationRenderer
@ -843,6 +842,9 @@ public protocol SharedAccountContext: AnyObject {
var hasOngoingCall: ValuePromise<Bool> { get }
var immediateHasOngoingCall: Bool { get }
var enablePreloads: Promise<Bool> { get }
var hasPreloadBlockingContent: Promise<Bool> { get }
var hasGroupCallOnScreen: Signal<Bool, NoError> { get }
var currentGroupCallController: ViewController? { get }
@ -935,7 +937,6 @@ public protocol AccountContext: AnyObject {
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
var meshAnimationCache: MeshAnimationCache { get }
var animationCache: AnimationCache { get }
var animationRenderer: MultiAnimationRenderer { get }

View file

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

View file

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

View file

@ -3,7 +3,6 @@ import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import PresentationDataUtils
@ -17,6 +16,26 @@ import ConfettiEffect
import TelegramUniversalVideoContent
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 var averageProgressPerSecond: Double = 0.0
private var lastMeasurement: (Double, Float)?
@ -91,7 +110,7 @@ private final class ImportManager {
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.archivePath = archivePath
self.entries = entries
@ -234,8 +253,8 @@ private final class ImportManager {
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
let unpackedFile = Signal<EngineTempBox.File, ImportError> { subscriber in
let tempFile = EngineTempBox.shared.tempFile(fileName: entry.0.path)
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
let startTime = CACurrentMediaTime()
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) {
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)
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 var presentationData: PresentationData
fileprivate let cancel: () -> Void
fileprivate var peerId: PeerId
fileprivate var peerId: EnginePeer.Id
private let archivePath: String?
private let mainEntry: TempBoxFile
private let mainEntry: EngineTempBox.File
private let totalBytes: Int64
private let totalMediaBytes: Int64
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.cancel = cancel
self.peerId = peerId
@ -818,7 +837,7 @@ public final class ChatImportActivityScreen: ViewController {
self.progressEstimator = ProgressEstimator()
self.beganCompletion = false
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
let resolvedPeerId: Signal<EnginePeer.Id, ImportManager.ImportError>
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|> mapError { _ -> ImportManager.ImportError in

View file

@ -733,7 +733,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
let canRemove = false
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: channel, threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: nil, peer: .channel(channel), threadId: threadId, canRemove: canRemove, defaultSound: defaultSound, edit: true, updatePeerSound: { peerId, sound in
let _ = (updatePeerSound(peerId, sound)
|> deliverOnMainQueue).start(next: { _ in
})

View file

@ -909,12 +909,12 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
if self.controlsHistoryPreload, case .chatList(groupId: .root) = self.location {
self.context.account.viewTracker.chatListPreloadItems.set(combineLatest(queue: .mainQueue(),
context.sharedContext.hasOngoingCall.get(),
context.sharedContext.enablePreloads.get(),
itemNode.listNode.preloadItems.get(),
enablePreload
)
|> map { hasOngoingCall, preloadItems, enablePreload -> Set<ChatHistoryPreloadItem> in
if hasOngoingCall || !enablePreload {
|> map { enablePreloads, preloadItems, enablePreload -> Set<ChatHistoryPreloadItem> in
if !enablePreloads || !enablePreload {
return Set()
} else {
return Set(preloadItems)

View file

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

View file

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

View file

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

View file

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

View file

@ -665,13 +665,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
}
case .hide:
switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default:
break
}
@ -966,13 +962,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
}
case .hide:
switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default:
break
}
@ -1844,19 +1836,16 @@ public final class ChatListNode: ListView {
suggestedChatListNotice,
savedMessagesPeer,
chatListViewUpdate,
self.chatFolderUpdates.get() |> distinctUntilChanged,
self.statePromise.get(),
contacts
)
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state, contacts) -> Signal<ChatListNodeListViewTransition, NoError> in
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, state, contacts) -> Signal<ChatListNodeListViewTransition, NoError> in
let (update, filter) = updateAndFilter
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
let notice: ChatListNotice?
if let chatFolderUpdates, chatFolderUpdates.availableChatsToJoin != 0 {
notice = .chatFolderUpdates(count: chatFolderUpdates.availableChatsToJoin)
} else if let suggestedChatListNotice {
if let suggestedChatListNotice {
notice = suggestedChatListNotice
} else if let storageInfo {
notice = .clearStorage(sizeFraction: storageInfo)

View file

@ -85,7 +85,6 @@ enum ChatListNotice: Equatable {
case premiumUpgrade(discount: Int32)
case premiumAnnualDiscount(discount: Int32)
case premiumRestore(discount: Int32)
case chatFolderUpdates(count: Int)
}
enum ChatListNodeEntry: Comparable, Identifiable {

View file

@ -182,15 +182,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .chatFolderUpdates(count):
let rawTitleString = item.strings.ChatList_ChatFolderUpdateHintTitle(item.strings.ChatList_ChatFolderUpdateCount(Int32(count)))
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString.string, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
if let range = rawTitleString.ranges.first {
titleStringValue.addAttribute(.foregroundColor, value: item.theme.rootController.navigationBar.accentTextColor, range: range.range)
}
titleString = titleStringValue
textString = NSAttributedString(string: item.strings.ChatList_ChatFolderUpdateHintText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
}
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - sideInset - rightInset, height: 100.0)))
@ -229,8 +220,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
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:
strongSelf.setRevealOptions((left: [], right: []))
}

View file

@ -92,7 +92,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case inlineForums(Bool)
case localTranscription(Bool)
case enableReactionOverrides(Bool)
case playerEmbedding(Bool)
case storiesExperiment(Bool)
case playlistPlayback(Bool)
case enableQuickReactionSwitch(Bool)
case voiceConference
@ -118,7 +118,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue
case .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
case .clearTips, .resetNotifications, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
return DebugControllerSection.experiments.rawValue
case .logTranslationRecognition, .resetTranslationStates:
return DebugControllerSection.translation.rawValue
@ -213,7 +213,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 41
case .resetTranslationStates:
return 42
case .playerEmbedding:
case .storiesExperiment:
return 43
case .playlistPlayback:
return 44
@ -1220,12 +1220,12 @@ private enum DebugControllerEntry: ItemListNodeEntry {
})
}).start()
})
case let .playerEmbedding(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
case let .storiesExperiment(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Gallery X", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
settings.playerEmbedding = value
settings.storiesExperiment = value
return PreferencesEntry(settings)
})
}).start()
@ -1384,7 +1384,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.logTranslationRecognition(experimentalSettings.logLanguageRecognition))
entries.append(.resetTranslationStates)
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
entries.append(.storiesExperiment(experimentalSettings.storiesExperiment))
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction))
}

View file

@ -104,7 +104,7 @@ public extension String {
var emojis: [String] {
var emojis: [String] = []
self.enumerateSubstrings(in: self.startIndex ..< self.endIndex, options: .byComposedCharacterSequences) { substring, _, _, _ in
if let substring = substring {
if let substring = substring, substring.isSingleEmoji {
emojis.append(substring)
}
}

View file

@ -1,12 +1,11 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import UniversalMediaPlayer
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 {
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
if let value = file.duration {
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 {
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
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 {
return combineLatest(messageMediaImageStatus(context: context, messageId: message.id, image: image), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in

View file

@ -24,6 +24,8 @@ swift_library(
"//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI",
"//submodules/MediaResources:MediaResources",
"//submodules/WebsiteType:WebsiteType",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/Stories/StoryContentComponent",
],
visibility = [
"//visibility:public",

View file

@ -14,6 +14,8 @@ import PeerAvatarGalleryUI
import GalleryUI
import MediaResources
import WebsiteType
import StoryContainerScreen
import StoryContentComponent
public enum ChatMessageGalleryControllerData {
case url(String)
@ -28,19 +30,20 @@ public enum ChatMessageGalleryControllerData {
case chatAvatars(AvatarGalleryController, Media)
case theme(TelegramMediaFile)
case other(Media)
case story(Signal<StoryContainerScreen, NoError>)
}
private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, media: [MediaId: Media], counter: inout Int) -> [InstantPageGalleryEntry] {
switch block {
case let .image(id, caption, _, _):
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
return result
}
case let .video(id, caption, _, _):
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
return result
}
@ -79,7 +82,7 @@ public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galle
}
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 {
@ -120,7 +123,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
if case .suggestedProfilePhoto = action.action {
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
if case .photoUpdated = action.action {
@ -128,7 +131,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
} else {
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)
@ -267,6 +270,21 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
openChatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
}
if context.sharedContext.immediateExperimentalUISettings.storiesExperiment {
return .story(StoryChatContent.messages(
context: context,
messageId: message.id
)
|> take(1)
|> deliverOnMainQueue
|> map { initialContent in
return StoryContainerScreen(
context: context,
initialContent: initialContent
)
})
}
return .gallery(startState
|> deliverOnMainQueue
|> map { startState in

View file

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

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Vision
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import AccountContext
@ -27,8 +26,8 @@ private final class CachedImageRecognizedContent: Codable {
}
}
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId) -> Signal<CachedImageRecognizedContent?, NoError> {
let key = ValueBoxKey(length: 20)
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal<CachedImageRecognizedContent?, NoError> {
let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
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> {
let key = ValueBoxKey(length: 20)
private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
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 {
return .single([])
}

View file

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

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramUIPreferences
@ -87,23 +86,23 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
return
}
var signals: [Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError>] = []
var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = []
for sticker in strongSelf.stickerPack.stickers {
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)
|> map { result -> (UUID, StickerVerificationStatus, MediaResource?) in
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, EngineMediaResource?) in
switch result {
case .progress:
return (sticker.uuid, .loading, nil)
case let .complete(resource, mimeType):
if ["application/x-tgsticker", "video/webm"].contains(mimeType) {
return (sticker.uuid, .verified, resource)
return (sticker.uuid, .verified, EngineMediaResource(resource))
} else {
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))
})
}
@ -115,7 +114,7 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
}
var verifiedStickers = Set<UUID>()
var declinedStickers = Set<UUID>()
var uploadedStickerResources: [UUID: MediaResource] = [:]
var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
for (uuid, result, resource) in results {
switch result {
case .verified:

View file

@ -3,7 +3,6 @@ import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@ -52,8 +51,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
private let context: AccountContext
private var presentationData: PresentationData
private var stickerPack: ImportStickerPack?
var stickerResources: [UUID: MediaResource] = [:]
private var uploadedStickerResources: [UUID: MediaResource] = [:]
var stickerResources: [UUID: EngineMediaResource] = [:]
private var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
private var stickerPackReady = true
private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -623,11 +622,11 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
}
if let resource = self.uploadedStickerResources[item.stickerItem.uuid] {
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 {
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?
@ -695,23 +694,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
let context = strongSelf.context
Queue.mainQueue().after(1.0) {
var firstItem: 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: [])
}
let firstItem: StickerPackItem? = firstStickerItem?.stickerPackItem
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 {
(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.uploadedStickerResources = uploadedStickerResources
var updatedItems: [StickerPackPreviewGridEntry] = []
@ -813,8 +796,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
} else {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data)
item.resource = resource
self.stickerResources[item.uuid] = resource
item.resource = EngineMediaResource(resource)
self.stickerResources[item.uuid] = EngineMediaResource(resource)
}
var isInitiallyVerified = false
if case .image = item.content {

View file

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

View file

@ -4,7 +4,6 @@ import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import StickerResources
import AccountContext
import AnimatedStickerNode
@ -142,7 +141,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
if case .video = stickerItem.content {
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
} else {

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import StickerResources
@ -87,7 +86,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
if case .video = item.content {
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
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData
@ -20,11 +19,11 @@ public final class InstantPageArticleItem: InstantPageItem {
let contentSize: CGSize
let cover: TelegramMediaImage?
let url: String
let webpageId: MediaId
let webpageId: EngineMedia.Id
let rtl: 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.userLocation = userLocation
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 imageSpacing: CGFloat = 10.0
var sideInset = inset

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
@ -20,14 +19,14 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
private var imageNode: TransformImageNode?
let url: String
let webpageId: MediaId
let webpageId: EngineMedia.Id
let cover: TelegramMediaImage?
private let openUrl: (InstantPageUrlItem) -> Void
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.url = url
self.webpageId = webpageId

View file

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

View file

@ -1,7 +1,6 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import Display
@ -36,7 +35,7 @@ private func generatePauseButton(color: UIColor) -> UIImage? {
private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString {
let string = NSMutableAttributedString()
if let file = media.media as? TelegramMediaFile {
if case let .file(file) = media.media {
loop: for attribute in file.attributes {
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
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: []))
let playlistType: MediaManagerPlayerType
if let file = self.media.media as? TelegramMediaFile {
if case let .file(file) = self.media.media {
playlistType = file.isVoice ? .voice : .music
} else {
playlistType = .music

View file

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

View file

@ -555,7 +555,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
if item is InstantPageWebEmbedItem {
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
}
if item is InstantPageDetailsItem {
@ -1003,17 +1003,17 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
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
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 _ = 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
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 _ = 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
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)
}
})], catchTapsOutside: true)
@ -1406,7 +1406,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
return
}
if let map = media.media as? TelegramMediaMap {
if case let .geo(map) = media.media {
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
}, stopLiveLocation: { _ in
}, openUrl: { _ in }, openPeer: { _ in
@ -1420,12 +1420,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
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 initialIndex = 0
for item in items {
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 {
initialIndex = medias.count
}
@ -1440,16 +1440,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
var fromPlayingVideo = false
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))
} else if let file = media.media as? TelegramMediaFile, file.isAnimated {
} else if case let .file(file) = media.media, file.isAnimated {
fromPlayingVideo = true
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
} else {
fromPlayingVideo = true
var medias: [InstantPageMedia] = mediasFromItems(items)
medias = medias.filter {
return $0.media is TelegramMediaImage || $0.media is TelegramMediaFile
medias = medias.filter { item in
switch item.media {
case .image, .file:
return true
default:
return false
}
}
for media in medias {

View file

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

View file

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

View file

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

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
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)
} else if let file = self.media.media as? TelegramMediaFile {
} else if case let .file(file) = self.media.media {
if file.isVideo {
var indexData: GalleryItemIndexData?
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: [])
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") {
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

View file

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

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
@ -43,7 +42,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private var currentSize: CGSize?
private var fetchStatus: MediaResourceStatus?
private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable()
@ -72,7 +71,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
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)
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
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = status
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus()
}
}
@ -105,7 +104,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
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)
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) {
@ -119,7 +118,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
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)
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))
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)
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())
@ -211,7 +210,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
if self.currentSize != size || self.themeUpdated {
self.currentSize = size
self.themeUpdated = false
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchContainerNode.update(size: size, transition: .immediate)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
@ -219,7 +218,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
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)
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 boundingSize = size
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()
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 imageSize = dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor))
apply()
} else if self.media.media is TelegramMediaMap {
} else if case .geo = self.media.media {
for attribute in self.attributes {
if let mapAttribute = attribute as? InstantPageMapAttribute {
let imageSize = mapAttribute.dimensions.aspectFilled(size)
@ -254,7 +253,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
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)
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 boundingSize = size
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:
switch gesture {
case .tap:
if self.media.media is TelegramMediaImage && self.media.index == -1 {
if case .image = self.media.media, self.media.index == -1 {
return
}
self.openMedia(self.media)
@ -311,7 +310,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
} else {
switch gesture {
case .tap:
if self.media.media is TelegramMediaImage && self.media.index == -1 {
if case .image = self.media.media, self.media.index == -1 {
return
}
self.openMedia(self.media)

View file

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

View file

@ -1,7 +1,6 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import Display
import TelegramPresentationData
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
var items: [InstantPageItem] = []
@ -373,7 +372,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
contentSize.height += verticalInset
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
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
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)
}
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)
contentSize.height += filledSize.height
@ -413,7 +412,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
}
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
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] = []
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)
} 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)
}
@ -460,11 +459,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var size = CGSize()
switch subItem {
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
}
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
}
default:
@ -502,9 +501,15 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var items: [InstantPageItem] = []
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 {
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)
avatarInset += 62.0
@ -572,7 +577,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
for subBlock in subItems {
switch subBlock {
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
mediaIndexCounter += 1
@ -583,7 +588,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
if let url = url {
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
default:
@ -626,11 +631,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize: CGSize
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
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 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 {
item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling)
@ -665,7 +670,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
}
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)
if offset.isZero {
contentSize.height += 40.0
@ -679,10 +684,10 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize = CGSize(width: boundingWidth, height: 0.0)
var items: [InstantPageItem] = []
if let file = media[audioId] as? TelegramMediaFile {
if case let .file(file) = media[audioId] {
let mediaIndex = mediaIndexCounter
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
items.append(item)
@ -765,7 +770,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
for (i, article) in articles.enumerated() {
var cover: TelegramMediaImage?
if let coverId = article.photoId {
cover = media[coverId] as? TelegramMediaImage
if case let .image(image) = media[coverId] {
cover = image
}
}
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 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)
contentSize.height += filledSize.height
@ -850,12 +857,12 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLoc
var contentSize = CGSize(width: boundingWidth, height: 0.0)
var items: [InstantPageItem] = []
var media = instantPage.media
var media = instantPage.media.mapValues(EngineMedia.init)
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 {
media[id] = video
media[id] = .file(video)
}
var mediaIndexCounter: Int = 0

View file

@ -1,15 +1,14 @@
import Foundation
import Postbox
import TelegramCore
public struct InstantPageMedia: Equatable {
public let index: Int
public let media: Media
public let media: EngineMedia
public let url: InstantPageUrlItem?
public let caption: 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.media = media
self.url = url
@ -18,6 +17,6 @@ public struct InstantPageMedia: Equatable {
}
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 UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import AccountContext
@ -22,7 +21,11 @@ struct InstantPageMediaPlaylistItemId: SharedMediaPlaylistItemId {
}
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 {
@ -114,7 +117,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
}
struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
let webpageId: MediaId
let webpageId: EngineMedia.Id
func isEqual(to: SharedMediaPlaylistId) -> Bool {
if let to = to as? InstantPageMediaPlaylistId {
@ -125,7 +128,7 @@ struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
}
struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation {
let webpageId: MediaId
let webpageId: EngineMedia.Id
func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
guard let to = to as? InstantPagePlaylistLocation else {

View file

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData
@ -14,12 +13,12 @@ public final class InstantPagePeerReferenceItem: InstantPageItem {
public let separatesTiles: Bool = false
public let medias: [InstantPageMedia] = []
let initialPeer: Peer
let initialPeer: EnginePeer
let safeInset: CGFloat
let transparent: 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.initialPeer = initialPeer
self.safeInset = safeInset

View file

@ -1,7 +1,6 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import Display
@ -64,13 +63,13 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
private let activityIndicator: ActivityIndicator
private let checkNode: ASImageNode
var peer: Peer?
var peer: EnginePeer?
private var peerDisposable: Disposable?
private let joinDisposable = MetaDisposable()
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.strings = strings
self.nameDisplayOrder = nameDisplayOrder
@ -147,26 +146,26 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
let account = self.context.account
let context = self.context
let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer)
|> mapToSignal({ peer -> Signal<Peer, NoError> in
let signal: Signal<EnginePeer, NoError> = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer._asPeer())
|> mapToSignal({ peer -> Signal<EnginePeer, NoError> in
if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil {
return .single(peer) |> then(context.engine.peers.resolvePeerByName(name: username)
|> mapToSignal({ updatedPeer -> Signal<Peer, NoError> in
return .single(.channel(peer)) |> then(context.engine.peers.resolvePeerByName(name: username)
|> mapToSignal({ updatedPeer -> Signal<EnginePeer, NoError> in
if let updatedPeer = updatedPeer {
return .single(updatedPeer._asPeer())
return .single(updatedPeer)
} else {
return .single(peer)
return .single(.channel(peer))
}
}))
} else {
return .single(peer)
return .single(EnginePeer(peer))
}
})
self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
strongSelf.peer = peer
if let peer = peer as? TelegramChannel {
if case let .channel(peer) = peer {
var joinState = strongSelf.joinState
if case .member = peer.participationStatus {
switch joinState {
@ -210,7 +209,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
private func applyThemeAndStrings(themeUpdated: Bool) {
if let peer = self.peer {
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
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() {
if let peer = self.peer {
self.openPeer(EnginePeer(peer))
self.openPeer(peer)
}
}

View file

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

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
@ -29,7 +28,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
private var currentSize: CGSize?
private var fetchStatus: MediaResourceStatus?
private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable()
@ -47,17 +46,19 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
self.openMedia = openMedia
var imageReference: ImageMediaReference?
if let file = media.media as? TelegramMediaFile, let presentation = smallestImageRepresentation(file.previewRepresentations) {
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
if case let .file(file) = media.media, let presentation = smallestImageRepresentation(file.previewRepresentations) {
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)
}
var streamVideo = false
if let file = media.media as? TelegramMediaFile {
var fileValue: TelegramMediaFile?
if case let .file(file) = media.media {
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.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
@ -66,13 +67,13 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
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.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = status
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus()
}
}

View file

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

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SafariServices
import TelegramPresentationData
@ -197,9 +196,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
if self.contentNode == nil || self.contentNode?.frame.width != width {
self.contentNode?.removeFromSupernode()
var media: [MediaId: Media] = [:]
var media: [EngineMedia.Id: EngineMedia] = [:]
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

View file

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

View file

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

View file

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

View file

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

View file

@ -186,9 +186,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe
private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode {
let media = self.items[index]
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)
} else if let _ = media.media as? TelegramMediaFile {
} else if case .file = media.media {
contentNode = ASDisplayNode()
} else {
contentNode = ASDisplayNode()

View file

@ -1,7 +1,6 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
@ -58,7 +57,7 @@ public final class InstantPageStoredState: Codable {
}
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)
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> {
let key = ValueBoxKey(length: 8)
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: webPage.webpageId.id)
if let state = state {

View file

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

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import Display
import TelegramPresentationData
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 {
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 TelegramCore
import Display
import Postbox
import AsyncDisplayKit
import TelegramPresentationData
import TelegramUIPreferences
@ -12,9 +11,9 @@ import ContextUI
public final class InstantPageUrlItem: Equatable {
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.webpageId = webpageId
}
@ -36,7 +35,7 @@ struct InstantPageTextStrikethroughItem {
struct InstantPageTextImageItem {
let frame: CGRect
let range: NSRange
let id: MediaId
let id: EngineMedia.Id
}
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 {
return (nil, [], CGSize())
}
@ -771,7 +770,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
}
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 {
let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
for imageItem in line.imageItems {
if let image = media[imageItem.id] as? TelegramMediaFile {
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)
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(image), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
additionalItems.append(item)
if item.frame.minY < topInset {

View file

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

View file

@ -389,7 +389,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
avatarListWrapperNode.contentNode.clipsToBounds = true
avatarListNode.backgroundColor = .clear
avatarListNode.peer = peer
avatarListNode.peer = EnginePeer(peer)
avatarListNode.firstFullSizeOnly = true
avatarListNode.offsetLocation = true
avatarListNode.customCenterTapAction = { [weak self] in
@ -405,7 +405,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
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.avatarListWrapperNode = avatarListWrapperNode

View file

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import LegacyComponents
import SwiftSignalKit
@ -49,8 +48,8 @@ public final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUplo
private var path: String?
private var size: Int?
private let data = Promise<MediaResourceData>()
private let dataValue = Atomic<MediaResourceData?>(value: nil)
private let data = Promise<EngineMediaResource.ResourceData>()
private let dataValue = Atomic<EngineMediaResource.ResourceData?>(value: nil)
public init(context: AccountContext) {
self.context = context
@ -70,14 +69,13 @@ public final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUplo
strongSelf.size = size
let result = strongSelf.dataValue.modify { dataValue in
if let dataValue = dataValue, dataValue.complete {
return MediaResourceData(path: path, offset: 0, size: Int64(size), complete: true)
if let dataValue = dataValue, dataValue.isComplete {
return EngineMediaResource.ResourceData(path: path, availableSize: Int64(size), isComplete: true)
} else {
return MediaResourceData(path: path, offset: 0, size: Int64(size), complete: false)
return EngineMediaResource.ResourceData(path: path, availableSize: Int64(size), isComplete: false)
}
}
if let result = result {
print("**set1 \(result) \(result.complete)")
strongSelf.data.set(.single(result))
}
}
@ -89,17 +87,15 @@ public final class LegacyLiveUploadInterface: VideoConversionWatcher, TGLiveUplo
override public func fileUpdated(_ completed: Bool) -> Any! {
let _ = super.fileUpdated(completed)
print("**fileUpdated \(completed)")
if completed {
let result = self.dataValue.modify { dataValue in
if let dataValue = dataValue {
return MediaResourceData(path: dataValue.path, offset: dataValue.offset, size: dataValue.size, complete: true)
return EngineMediaResource.ResourceData(path: dataValue.path, availableSize: dataValue.availableSize, isComplete: true)
} else {
return nil
}
}
if let result = result {
print("**set2 \(result) \(completed)")
self.data.set(.single(result))
return LegacyLiveUploadInterfaceResult(id: self.id)
} else {

View file

@ -878,7 +878,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
if statusUpdated && item.displayFileInfo {
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
if case .Fetching = value.fetchStatus, !item.isDownloadList {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
@ -905,10 +905,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
}
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 {
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
if case .Fetching = value.fetchStatus, !item.isDownloadList {
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
}
}

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