mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
0f0b14833f
commit
b80f2636d6
@ -41,7 +41,8 @@ public final class HStack<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
size.width += child.size.width
|
||||
size.height = max(size.height, child.size.height)
|
||||
}
|
||||
|
||||
size.width += context.component.spacing * CGFloat(updatedChildren.count - 1)
|
||||
|
||||
var nextX = 0.0
|
||||
for child in updatedChildren {
|
||||
context.add(child
|
||||
|
@ -46,9 +46,10 @@ public final class Image: Component {
|
||||
|
||||
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.image = component.image
|
||||
self.tintColor = component.tintColor
|
||||
self.contentMode = component.contentMode
|
||||
|
||||
transition.setTintColor(view: self, color: component.tintColor ?? .white)
|
||||
|
||||
return component.size ?? availableSize
|
||||
}
|
||||
}
|
||||
|
@ -573,6 +573,7 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
case peers
|
||||
case boostPeer
|
||||
case additionalPeerIds
|
||||
case countries
|
||||
case onlyNewSubscribers
|
||||
case randomId
|
||||
case untilDate
|
||||
@ -592,7 +593,7 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
case restore
|
||||
case gift(peerId: EnginePeer.Id)
|
||||
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?)
|
||||
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32)
|
||||
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32)
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
@ -618,6 +619,7 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
self = .giveaway(
|
||||
boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)),
|
||||
additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) },
|
||||
countries: try container.decodeIfPresent([String].self, forKey: .countries) ?? [],
|
||||
onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers),
|
||||
randomId: try container.decode(Int64.self, forKey: .randomId),
|
||||
untilDate: try container.decode(Int32.self, forKey: .untilDate)
|
||||
@ -644,10 +646,11 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
try container.encode(PurposeType.giftCode.rawValue, forKey: .type)
|
||||
try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers)
|
||||
try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer)
|
||||
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate):
|
||||
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate):
|
||||
try container.encode(PurposeType.giveaway.rawValue, forKey: .type)
|
||||
try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
|
||||
try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds)
|
||||
try container.encode(countries, forKey: .countries)
|
||||
try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers)
|
||||
try container.encode(randomId, forKey: .randomId)
|
||||
try container.encode(untilDate, forKey: .untilDate)
|
||||
@ -666,8 +669,8 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
self = .gift(peerId: peerId)
|
||||
case let .giftCode(peerIds, boostPeer, _, _):
|
||||
self = .giftCode(peerIds: peerIds, boostPeer: boostPeer)
|
||||
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate, _, _):
|
||||
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
|
||||
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, _, _):
|
||||
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -684,8 +687,8 @@ private final class PendingInAppPurchaseState: Codable {
|
||||
return .gift(peerId: peerId, currency: currency, amount: amount)
|
||||
case let .giftCode(peerIds, boostPeer):
|
||||
return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount)
|
||||
case let .giveaway(boostPeer, additionalPeerIds, onlyNewSubscribers, randomId, untilDate):
|
||||
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
|
||||
case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate):
|
||||
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -598,6 +598,7 @@ private struct CreateGiveawayControllerState: Equatable {
|
||||
var channels: [EnginePeer.Id]
|
||||
var peers: [EnginePeer.Id]
|
||||
var selectedMonths: Int32?
|
||||
var countries: [String]
|
||||
var onlyNewEligible: Bool
|
||||
var time: Int32
|
||||
var pickingTimeLimit = false
|
||||
@ -620,7 +621,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
||||
}
|
||||
|
||||
let expiryTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + 86400 * 5
|
||||
let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, channels: [], peers: [], onlyNewEligible: false, time: expiryTime)
|
||||
let initialState: CreateGiveawayControllerState = CreateGiveawayControllerState(mode: .giveaway, subscriptions: initialSubscriptions, channels: [], peers: [], countries: [], onlyNewEligible: false, time: expiryTime)
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
@ -790,11 +791,14 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
||||
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
|
||||
|
||||
let purpose: AppStoreTransactionPurpose
|
||||
let quantity: Int32
|
||||
switch state.mode {
|
||||
case .giveaway:
|
||||
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
|
||||
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
|
||||
quantity = selectedProduct.giftOption.storeQuantity
|
||||
case .gift:
|
||||
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
|
||||
quantity = Int32(state.peers.count)
|
||||
}
|
||||
|
||||
updateState { state in
|
||||
@ -808,7 +812,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
||||
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
|
||||
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: selectedProduct.giftOption.storeQuantity, purpose: purpose)
|
||||
let _ = (inAppPurchaseManager.buyProduct(selectedProduct.storeProduct, quantity: quantity, purpose: purpose)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak controller] status in
|
||||
if case .purchased = status {
|
||||
if let controller, let navigationController = controller.navigationController as? NavigationController {
|
||||
@ -890,7 +894,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
||||
}
|
||||
})
|
||||
case let .prepaid(prepaidGiveaway):
|
||||
let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time)
|
||||
let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
if let controller, let navigationController = controller.navigationController as? NavigationController {
|
||||
var controllers = navigationController.viewControllers
|
||||
|
@ -23,6 +23,7 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
public let maxStoriesMonthlyCount: Int32
|
||||
public let maxStoriesSuggestedReactions: Int32
|
||||
public let maxGiveawayChannelsCount: Int32
|
||||
public let maxGiveawayCountriesCount: Int32
|
||||
|
||||
public static var defaultValue: UserLimitsConfiguration {
|
||||
return UserLimitsConfiguration(
|
||||
@ -46,7 +47,8 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
maxStoriesWeeklyCount: 7,
|
||||
maxStoriesMonthlyCount: 30,
|
||||
maxStoriesSuggestedReactions: 1,
|
||||
maxGiveawayChannelsCount: 10
|
||||
maxGiveawayChannelsCount: 10,
|
||||
maxGiveawayCountriesCount: 10
|
||||
)
|
||||
}
|
||||
|
||||
@ -71,7 +73,8 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
maxStoriesWeeklyCount: Int32,
|
||||
maxStoriesMonthlyCount: Int32,
|
||||
maxStoriesSuggestedReactions: Int32,
|
||||
maxGiveawayChannelsCount: Int32
|
||||
maxGiveawayChannelsCount: Int32,
|
||||
maxGiveawayCountriesCount: Int32
|
||||
) {
|
||||
self.maxPinnedChatCount = maxPinnedChatCount
|
||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||
@ -94,6 +97,7 @@ public struct UserLimitsConfiguration: Equatable {
|
||||
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
|
||||
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
|
||||
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
|
||||
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,5 +143,6 @@ extension UserLimitsConfiguration {
|
||||
self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount)
|
||||
self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount)
|
||||
self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount)
|
||||
self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount)
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ public enum EngineConfiguration {
|
||||
public let maxStoriesMonthlyCount: Int32
|
||||
public let maxStoriesSuggestedReactions: Int32
|
||||
public let maxGiveawayChannelsCount: Int32
|
||||
public let maxGiveawayCountriesCount: Int32
|
||||
|
||||
public static var defaultValue: UserLimits {
|
||||
return UserLimits(UserLimitsConfiguration.defaultValue)
|
||||
@ -83,7 +84,8 @@ public enum EngineConfiguration {
|
||||
maxStoriesWeeklyCount: Int32,
|
||||
maxStoriesMonthlyCount: Int32,
|
||||
maxStoriesSuggestedReactions: Int32,
|
||||
maxGiveawayChannelsCount: Int32
|
||||
maxGiveawayChannelsCount: Int32,
|
||||
maxGiveawayCountriesCount: Int32
|
||||
) {
|
||||
self.maxPinnedChatCount = maxPinnedChatCount
|
||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||
@ -106,6 +108,7 @@ public enum EngineConfiguration {
|
||||
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
|
||||
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
|
||||
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
|
||||
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,7 +166,8 @@ public extension EngineConfiguration.UserLimits {
|
||||
maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount,
|
||||
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount,
|
||||
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions,
|
||||
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount
|
||||
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount,
|
||||
maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ public enum AppStoreTransactionPurpose {
|
||||
case restore
|
||||
case gift(peerId: EnginePeer.Id, currency: String, amount: Int64)
|
||||
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64)
|
||||
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
|
||||
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32, currency: String, amount: Int64)
|
||||
}
|
||||
|
||||
private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTransactionPurpose) -> Signal<Api.InputStorePaymentPurpose, NoError> {
|
||||
@ -59,7 +59,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
|
||||
|
||||
return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount)
|
||||
}
|
||||
case let .giveaway(boostPeerId, additionalPeerIds, onlyNewSubscribers, randomId, untilDate, currency, amount):
|
||||
case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, randomId, untilDate, currency, amount):
|
||||
return account.postbox.transaction { transaction -> Signal<Api.InputStorePaymentPurpose, NoError> in
|
||||
guard let peer = transaction.getPeer(boostPeerId), let apiBoostPeer = apiInputPeer(peer) else {
|
||||
return .complete()
|
||||
@ -77,7 +77,10 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: nil, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
|
||||
if !countries.isEmpty {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
return .single(.inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount))
|
||||
}
|
||||
|> switchToLatest
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ func _internal_applyPremiumGiftCode(account: Account, slug: String) -> Signal<Ne
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
|
||||
func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||
var flags: Int32 = 0
|
||||
if onlyNewSubscribers {
|
||||
@ -206,10 +206,14 @@ func _internal_launchPrepaidGiveaway(account: Account, peerId: EnginePeer.Id, id
|
||||
}
|
||||
}
|
||||
|
||||
if !countries.isEmpty {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
|
||||
guard let inputPeer = inputPeer else {
|
||||
return .complete()
|
||||
}
|
||||
return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, countriesIso2: nil, randomId: randomId, untilDate: untilDate, currency: "", amount: 0)))
|
||||
return account.network.request(Api.functions.payments.launchPrepaidGiveaway(peer: inputPeer, giveawayId: id, purpose: .inputStorePaymentPremiumGiveaway(flags: flags, boostPeer: inputPeer, additionalPeers: additionalPeers, countriesIso2: countries, randomId: randomId, untilDate: untilDate, currency: "", amount: 0)))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
|
@ -62,8 +62,8 @@ public extension TelegramEngine {
|
||||
return _internal_getPremiumGiveawayInfo(account: self.account, peerId: peerId, messageId: messageId)
|
||||
}
|
||||
|
||||
public func launchPrepaidGiveaway(peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
|
||||
return _internal_launchPrepaidGiveaway(account: self.account, peerId: peerId, id: id, additionalPeerIds: additionalPeerIds, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
|
||||
public func launchPrepaidGiveaway(peerId: EnginePeer.Id, id: Int64, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, randomId: Int64, untilDate: Int32) -> Signal<Never, NoError> {
|
||||
return _internal_launchPrepaidGiveaway(account: self.account, peerId: peerId, id: id, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, randomId: randomId, untilDate: untilDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ public final class CameraButton: Component {
|
||||
}
|
||||
|
||||
public final class View: UIButton, ComponentTaggedView {
|
||||
private let containerView = UIView()
|
||||
public var contentView: ComponentHostView<Empty>
|
||||
|
||||
private var component: CameraButton?
|
||||
@ -75,12 +76,14 @@ public final class CameraButton: Component {
|
||||
} else {
|
||||
scale = 1.0
|
||||
}
|
||||
transition.setScale(view: self, scale: scale)
|
||||
transition.setScale(view: self.containerView, scale: scale)
|
||||
}
|
||||
|
||||
private var longTapGestureRecognizer: UILongPressGestureRecognizer?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.containerView.isUserInteractionEnabled = false
|
||||
|
||||
self.contentView = ComponentHostView<Empty>()
|
||||
self.contentView.isUserInteractionEnabled = false
|
||||
self.contentView.layer.allowsGroupOpacity = true
|
||||
@ -89,7 +92,8 @@ public final class CameraButton: Component {
|
||||
|
||||
self.isExclusiveTouch = true
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
self.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.contentView)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
@ -145,12 +149,12 @@ public final class CameraButton: Component {
|
||||
self.contentView = ComponentHostView<Empty>()
|
||||
self.contentView.isUserInteractionEnabled = false
|
||||
self.contentView.layer.allowsGroupOpacity = true
|
||||
self.addSubview(self.contentView)
|
||||
self.containerView.addSubview(self.contentView)
|
||||
|
||||
if transition.animation.isImmediate {
|
||||
previousContentView.removeFromSuperview()
|
||||
} else {
|
||||
self.addSubview(previousContentView)
|
||||
self.containerView.addSubview(previousContentView)
|
||||
previousContentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in
|
||||
previousContentView?.removeFromSuperview()
|
||||
})
|
||||
@ -175,7 +179,11 @@ public final class CameraButton: Component {
|
||||
self.isEnabled = component.isEnabled
|
||||
self.longTapGestureRecognizer?.isEnabled = component.longTapAction != nil
|
||||
|
||||
self.contentView.frame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize)
|
||||
self.contentView.bounds = CGRect(origin: .zero, size: contentSize)
|
||||
self.contentView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
|
||||
self.containerView.bounds = CGRect(origin: .zero, size: size)
|
||||
self.containerView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
|
||||
return size
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ swift_library(
|
||||
"//submodules/Utils/VolumeButtons",
|
||||
"//submodules/TelegramNotices",
|
||||
"//submodules/DeviceAccess",
|
||||
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
|
||||
|
||||
],
|
||||
visibility = [
|
||||
|
@ -30,16 +30,27 @@ enum CameraMode: Equatable {
|
||||
case video
|
||||
}
|
||||
|
||||
private struct CameraState: Equatable {
|
||||
struct CameraState: Equatable {
|
||||
enum Recording: Equatable {
|
||||
case none
|
||||
case holding
|
||||
case handsFree
|
||||
}
|
||||
enum FlashTint {
|
||||
enum FlashTint: Equatable {
|
||||
case white
|
||||
case yellow
|
||||
case blue
|
||||
|
||||
var color: UIColor {
|
||||
switch self {
|
||||
case .white:
|
||||
return .white
|
||||
case .yellow:
|
||||
return UIColor(rgb: 0xffed8c)
|
||||
case .blue:
|
||||
return UIColor(rgb: 0x8cdfff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mode: CameraMode
|
||||
@ -225,6 +236,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
var swipeHint: CaptureControlsComponent.SwipeHint = .none
|
||||
var isTransitioning = false
|
||||
|
||||
var displayingFlashTint = false
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
init(
|
||||
@ -395,7 +408,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
camera.setFlashMode(.on)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func toggleFlashMode() {
|
||||
guard let controller = self.getController(), let camera = controller.camera else {
|
||||
return
|
||||
@ -415,6 +428,27 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
|
||||
func updateFlashTint(_ tint: CameraState.FlashTint?) {
|
||||
guard let controller = self.getController(), let camera = controller.camera else {
|
||||
return
|
||||
}
|
||||
if let tint {
|
||||
controller.updateCameraState({ $0.updatedFlashTint(tint) }, transition: .easeInOut(duration: 0.2))
|
||||
} else {
|
||||
camera.setFlashMode(.off)
|
||||
}
|
||||
}
|
||||
|
||||
func presentFlashTint() {
|
||||
guard let controller = self.getController(), let camera = controller.camera else {
|
||||
return
|
||||
}
|
||||
camera.setFlashMode(.on)
|
||||
|
||||
self.displayingFlashTint = true
|
||||
self.updated(transition: .immediate)
|
||||
}
|
||||
|
||||
private var lastFlipTimestamp: Double?
|
||||
func togglePosition(_ action: ActionSlot<Void>) {
|
||||
guard let controller = self.getController(), let camera = controller.camera else {
|
||||
@ -622,6 +656,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let dualButton = Child(CameraButton.self)
|
||||
let modeControl = Child(ModeComponent.self)
|
||||
let hintLabel = Child(HintLabelComponent.self)
|
||||
let flashTintControl = Child(FlashTintControlComponent.self)
|
||||
|
||||
let timeBackground = Child(RoundedRectangle.self)
|
||||
let timeLabel = Child(MultilineTextComponent.self)
|
||||
@ -713,19 +748,11 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
// )
|
||||
}
|
||||
|
||||
let displayFrontFlash = component.cameraState.recording != .none || component.cameraState.mode == .video || state.displayingFlashTint
|
||||
var controlsTintColor: UIColor = .white
|
||||
if case .front = component.cameraState.position, case .on = component.cameraState.flashMode {
|
||||
let flashTintColor: UIColor
|
||||
switch component.cameraState.flashTint {
|
||||
case .white:
|
||||
flashTintColor = .white
|
||||
case .yellow:
|
||||
flashTintColor = UIColor(rgb: 0xffed8c)
|
||||
case .blue:
|
||||
flashTintColor = UIColor(rgb: 0x8cdfff)
|
||||
}
|
||||
if case .front = component.cameraState.position, case .on = component.cameraState.flashMode, displayFrontFlash {
|
||||
let frontFlash = frontFlash.update(
|
||||
component: Image(image: state.image(.flashImage), tintColor: flashTintColor),
|
||||
component: Image(image: state.image(.flashImage), tintColor: component.cameraState.flashTint.color),
|
||||
availableSize: availableSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
@ -849,6 +876,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
.position(captureControlsPosition)
|
||||
)
|
||||
|
||||
var flashButtonPosition: CGPoint?
|
||||
let topControlInset: CGFloat = 20.0
|
||||
if case .none = component.cameraState.recording, !state.isTransitioning {
|
||||
let cancelButton = cancelButton.update(
|
||||
@ -928,13 +956,21 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if let state {
|
||||
state.toggleFlashMode()
|
||||
}
|
||||
},
|
||||
longTapAction: { [weak state] in
|
||||
if let state {
|
||||
state.presentFlashTint()
|
||||
}
|
||||
}
|
||||
).tagged(flashButtonTag),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let position = CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0)
|
||||
flashButtonPosition = position
|
||||
context.add(flashButton
|
||||
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
|
||||
.position(position)
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
)
|
||||
@ -1114,6 +1150,28 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
if let flashButtonPosition, state.displayingFlashTint {
|
||||
let flashTintControl = flashTintControl.update(
|
||||
component: FlashTintControlComponent(
|
||||
position: flashButtonPosition.offsetBy(dx: 0.0, dy: 27.0),
|
||||
tint: component.cameraState.flashTint,
|
||||
update: { [weak state] tint in
|
||||
state?.updateFlashTint(tint)
|
||||
},
|
||||
dismiss: { [weak state] in
|
||||
state?.displayingFlashTint = false
|
||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
),
|
||||
availableSize: availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(flashTintControl
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
}
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
@ -2446,6 +2504,12 @@ public class CameraScreen: ViewController {
|
||||
if isTablet && isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
|
||||
if self.cameraState.flashMode == .on && (self.cameraState.recording != .none || self.cameraState.mode == .video) {
|
||||
self.controller?.statusBarStyle = .Black
|
||||
} else {
|
||||
self.controller?.statusBarStyle = .White
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2743,13 +2807,12 @@ public class CameraScreen: ViewController {
|
||||
self.node.camera?.stopCapture(invalidate: true)
|
||||
self.isDismissed = true
|
||||
if animated {
|
||||
self.ignoreStatusBar = true
|
||||
if let layout = self.validLayout, layout.metrics.isTablet {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
self.node.animateOut(completion: {
|
||||
self.dismiss(animated: false)
|
||||
})
|
||||
} else {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
if !interactive {
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
|
||||
@ -2810,16 +2873,41 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var statusBarStyle: StatusBarStyle = .White {
|
||||
didSet {
|
||||
if self.statusBarStyle != oldValue {
|
||||
self.updateStatusBarAppearance()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var ignoreStatusBar = false {
|
||||
didSet {
|
||||
if self.ignoreStatusBar != oldValue {
|
||||
self.updateStatusBarAppearance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStatusBarAppearance() {
|
||||
let effectiveStatusBarStyle: StatusBarStyle
|
||||
if !self.ignoreStatusBar {
|
||||
effectiveStatusBarStyle = self.statusBarStyle
|
||||
} else {
|
||||
effectiveStatusBarStyle = .Ignore
|
||||
}
|
||||
self.statusBar.updateStatusBarStyle(effectiveStatusBarStyle, animated: true)
|
||||
}
|
||||
|
||||
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
|
||||
if let layout = self.validLayout, layout.metrics.isTablet {
|
||||
return
|
||||
}
|
||||
if dismissing {
|
||||
if transitionFraction < 0.7 || velocity < -1000.0 {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
self.ignoreStatusBar = true
|
||||
self.requestDismiss(animated: true, interactive: true)
|
||||
} else {
|
||||
self.statusBar.updateStatusBarStyle(.White, animated: true)
|
||||
self.ignoreStatusBar = false
|
||||
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
@ -2828,7 +2916,8 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
} else {
|
||||
if transitionFraction > 0.33 || velocity > 1000.0 {
|
||||
self.statusBar.updateStatusBarStyle(.White, animated: true)
|
||||
self.ignoreStatusBar = false
|
||||
self.updateStatusBarAppearance()
|
||||
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
@ -2837,7 +2926,8 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
self.ignoreStatusBar = true
|
||||
self.updateStatusBarAppearance()
|
||||
self.requestDismiss(animated: true, interactive: true)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,300 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import RoundedRectWithTailPath
|
||||
|
||||
private final class FlashColorComponent: Component {
|
||||
let tint: CameraState.FlashTint?
|
||||
let isSelected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
tint: CameraState.FlashTint?,
|
||||
isSelected: Bool,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.tint = tint
|
||||
self.isSelected = isSelected
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func == (lhs: FlashColorComponent, rhs: FlashColorComponent) -> Bool {
|
||||
return lhs.tint == rhs.tint && lhs.isSelected == rhs.isSelected
|
||||
}
|
||||
|
||||
final class View: UIButton {
|
||||
private var component: FlashColorComponent?
|
||||
|
||||
private var contentView: UIView
|
||||
|
||||
private let circleLayer: SimpleShapeLayer
|
||||
private var ringLayer: CALayer?
|
||||
private var iconLayer: CALayer?
|
||||
|
||||
private var currentIsHighlighted: Bool = false {
|
||||
didSet {
|
||||
if self.currentIsHighlighted != oldValue {
|
||||
self.contentView.alpha = self.currentIsHighlighted ? 0.6 : 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contentView = UIView(frame: CGRect(origin: .zero, size: frame.size))
|
||||
self.contentView.isUserInteractionEnabled = false
|
||||
self.circleLayer = SimpleShapeLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
self.contentView.layer.addSublayer(self.circleLayer)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.component?.action()
|
||||
}
|
||||
|
||||
override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
self.currentIsHighlighted = true
|
||||
|
||||
return super.beginTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
self.currentIsHighlighted = false
|
||||
|
||||
super.endTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override public func cancelTracking(with event: UIEvent?) {
|
||||
self.currentIsHighlighted = false
|
||||
|
||||
super.cancelTracking(with: event)
|
||||
}
|
||||
|
||||
func update(component: FlashColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
let contentSize = CGSize(width: 24.0, height: 24.0)
|
||||
self.contentView.frame = CGRect(origin: .zero, size: contentSize)
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: contentSize)
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.contentView.layer.allowsGroupOpacity = true
|
||||
|
||||
self.circleLayer.frame = bounds
|
||||
if self.ringLayer == nil {
|
||||
let ringLayer = SimpleLayer()
|
||||
ringLayer.backgroundColor = UIColor.clear.cgColor
|
||||
ringLayer.cornerRadius = contentSize.width / 2.0
|
||||
ringLayer.borderWidth = 1.0 + UIScreenPixel
|
||||
ringLayer.frame = CGRect(origin: .zero, size: contentSize)
|
||||
self.contentView.layer.insertSublayer(ringLayer, at: 0)
|
||||
self.ringLayer = ringLayer
|
||||
}
|
||||
|
||||
if component.isSelected {
|
||||
transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 3.0 - UIScreenPixel, dy: 3.0 - UIScreenPixel), transform: nil))
|
||||
} else {
|
||||
transition.setShapeLayerPath(layer: self.circleLayer, path: CGPath(ellipseIn: bounds, transform: nil))
|
||||
}
|
||||
|
||||
if let color = component.tint?.color {
|
||||
self.circleLayer.fillColor = color.cgColor
|
||||
self.ringLayer?.borderColor = color.cgColor
|
||||
} else {
|
||||
if self.iconLayer == nil {
|
||||
let iconLayer = SimpleLayer()
|
||||
iconLayer.contents = UIImage(bundleImageName: "Camera/FlashOffIcon")?.cgImage
|
||||
iconLayer.contentsGravity = .resizeAspect
|
||||
iconLayer.frame = bounds.insetBy(dx: -4.0, dy: -4.0)
|
||||
self.contentView.layer.addSublayer(iconLayer)
|
||||
self.iconLayer = iconLayer
|
||||
}
|
||||
|
||||
self.circleLayer.fillColor = UIColor(rgb: 0xffffff, alpha: 0.1).cgColor
|
||||
self.ringLayer?.borderColor = UIColor.clear.cgColor
|
||||
}
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class FlashTintControlComponent: Component {
|
||||
let position: CGPoint
|
||||
let tint: CameraState.FlashTint
|
||||
let update: (CameraState.FlashTint?) -> Void
|
||||
let dismiss: () -> Void
|
||||
|
||||
init(
|
||||
position: CGPoint,
|
||||
tint: CameraState.FlashTint,
|
||||
update: @escaping (CameraState.FlashTint?) -> Void,
|
||||
dismiss: @escaping () -> Void
|
||||
) {
|
||||
self.position = position
|
||||
self.tint = tint
|
||||
self.update = update
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func == (lhs: FlashTintControlComponent, rhs: FlashTintControlComponent) -> Bool {
|
||||
return lhs.position == rhs.position && lhs.tint == rhs.tint
|
||||
}
|
||||
|
||||
final class View: UIButton {
|
||||
private var component: FlashTintControlComponent?
|
||||
|
||||
private let dismissView = UIView()
|
||||
private let containerView = UIView()
|
||||
private let effectView: UIVisualEffectView
|
||||
private let maskLayer = CAShapeLayer()
|
||||
private let swatches = ComponentView<Empty>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.containerView.layer.anchorPoint = CGPoint(x: 0.8, y: 0.0)
|
||||
|
||||
self.addSubview(self.dismissView)
|
||||
self.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.effectView)
|
||||
|
||||
self.dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapped)))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func dismissTapped() {
|
||||
self.component?.dismiss()
|
||||
}
|
||||
|
||||
func update(component: FlashTintControlComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let isFirstTime = self.component == nil
|
||||
self.component = component
|
||||
|
||||
let size = CGSize(width: 160.0, height: 40.0)
|
||||
if isFirstTime {
|
||||
self.maskLayer.path = generateRoundedRectWithTailPath(rectSize: size, cornerRadius: 10.0, tailSize: CGSize(width: 18, height: 7.0), tailRadius: 1.0, tailPosition: 0.8, transformTail: false).cgPath
|
||||
self.maskLayer.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height + 7.0))
|
||||
self.effectView.layer.mask = self.maskLayer
|
||||
}
|
||||
|
||||
let swatchesSize = self.swatches.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
HStack(
|
||||
[
|
||||
AnyComponentWithIdentity(
|
||||
id: "off",
|
||||
component: AnyComponent(
|
||||
FlashColorComponent(
|
||||
tint: nil,
|
||||
isSelected: false,
|
||||
action: {
|
||||
component.update(nil)
|
||||
component.dismiss()
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "white",
|
||||
component: AnyComponent(
|
||||
FlashColorComponent(
|
||||
tint: .white,
|
||||
isSelected: component.tint == .white,
|
||||
action: {
|
||||
component.update(.white)
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "yellow",
|
||||
component: AnyComponent(
|
||||
FlashColorComponent(
|
||||
tint: .yellow,
|
||||
isSelected: component.tint == .yellow,
|
||||
action: {
|
||||
component.update(.yellow)
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "blue",
|
||||
component: AnyComponent(
|
||||
FlashColorComponent(
|
||||
tint: .blue,
|
||||
isSelected: component.tint == .blue,
|
||||
action: {
|
||||
component.update(.blue)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
],
|
||||
spacing: 16.0
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.swatches.view {
|
||||
if view.superview == nil {
|
||||
self.containerView.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - swatchesSize.width) / 2.0), y: floorToScreenPixels((size.height - swatchesSize.height) / 2.0)), size: swatchesSize)
|
||||
}
|
||||
|
||||
self.dismissView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
self.containerView.bounds = CGRect(origin: .zero, size: size)
|
||||
self.containerView.center = component.position
|
||||
|
||||
self.effectView.frame = CGRect(origin: CGPoint(x: 0.0, y: -7.0), size: CGSize(width: size.width, height: size.height + 7.0))
|
||||
|
||||
if isFirstTime {
|
||||
self.containerView.layer.animateScale(from: 0.0, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.containerView.frame.contains(point) {
|
||||
return self.dismissView
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -27,10 +27,12 @@ final class MediaNavigationStripComponent: Component {
|
||||
|
||||
let index: Int
|
||||
let count: Int
|
||||
let isSeeking: Bool
|
||||
|
||||
init(index: Int, count: Int) {
|
||||
init(index: Int, count: Int, isSeeking: Bool) {
|
||||
self.index = index
|
||||
self.count = count
|
||||
self.isSeeking = isSeeking
|
||||
}
|
||||
|
||||
static func ==(lhs: MediaNavigationStripComponent, rhs: MediaNavigationStripComponent) -> Bool {
|
||||
@ -40,6 +42,9 @@ final class MediaNavigationStripComponent: Component {
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
if lhs.isSeeking != rhs.isSeeking {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -155,12 +160,18 @@ final class MediaNavigationStripComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
guard !self.isTransitioning else {
|
||||
return
|
||||
}
|
||||
|
||||
let itemFrame = itemLayer.bounds
|
||||
transition.setFrame(layer: itemLayer.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: value * itemFrame.width, height: itemFrame.height)))
|
||||
itemLayer.updateIsBuffering(size: itemFrame.size, isBuffering: isBuffering)
|
||||
}
|
||||
|
||||
private var isTransitioning = false
|
||||
func update(component: MediaNavigationStripComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
@ -169,6 +180,10 @@ final class MediaNavigationStripComponent: Component {
|
||||
let itemHeight: CGFloat = 2.0
|
||||
let minItemWidth: CGFloat = 2.0
|
||||
|
||||
var size = CGSize(width: availableSize.width, height: itemHeight)
|
||||
|
||||
var didSetCompletion = false
|
||||
|
||||
var validIndices: [Int] = []
|
||||
if component.count != 0 {
|
||||
let idealItemWidth: CGFloat = (availableSize.width - CGFloat(component.count - 1) * spacing) / CGFloat(component.count)
|
||||
@ -205,8 +220,12 @@ final class MediaNavigationStripComponent: Component {
|
||||
if i >= component.count {
|
||||
continue
|
||||
}
|
||||
let itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))
|
||||
if itemFrame.maxY < 0.0 || itemFrame.minY >= availableSize.width {
|
||||
var itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))
|
||||
if component.isSeeking {
|
||||
itemFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: 6.0))
|
||||
size.height = itemFrame.height
|
||||
}
|
||||
if itemFrame.maxX < 0.0 || itemFrame.minX >= availableSize.width {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -219,11 +238,21 @@ final class MediaNavigationStripComponent: Component {
|
||||
itemLayer = ItemLayer()
|
||||
self.layer.addSublayer(itemLayer)
|
||||
self.visibleItems[i] = itemLayer
|
||||
itemLayer.cornerRadius = itemHeight * 0.5
|
||||
}
|
||||
|
||||
|
||||
transition.setFrame(layer: itemLayer, frame: itemFrame)
|
||||
|
||||
transition.setCornerRadius(layer: itemLayer, cornerRadius: itemFrame.height * 0.5)
|
||||
transition.setCornerRadius(layer: itemLayer.foregroundLayer, cornerRadius: itemFrame.height * 0.5, completion: transition.animation.isImmediate || didSetCompletion ? nil : { [weak self] _ in
|
||||
if let self {
|
||||
self.isTransitioning = false
|
||||
}
|
||||
})
|
||||
if !transition.animation.isImmediate && component.isSeeking != previousComponent?.isSeeking {
|
||||
self.isTransitioning = true
|
||||
didSetCompletion = true
|
||||
}
|
||||
|
||||
itemLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.5).cgColor
|
||||
itemLayer.foregroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 1.0).cgColor
|
||||
|
||||
@ -239,6 +268,9 @@ final class MediaNavigationStripComponent: Component {
|
||||
}
|
||||
|
||||
transition.setFrame(layer: itemLayer.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: itemProgress * itemFrame.width, height: itemFrame.height)))
|
||||
|
||||
transition.setAlpha(layer: itemLayer, alpha: !component.isSeeking || i == component.index ? 1.0 : 0.0)
|
||||
|
||||
itemLayer.updateIsBuffering(size: itemFrame.size, isBuffering: itemIsBuffering)
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +92,8 @@ private final class MuteMonitor {
|
||||
private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
|
||||
var shouldBegin: ((UITouch) -> Bool)?
|
||||
var updateIsTracking: ((CGPoint?) -> Void)?
|
||||
var updatePanMove: ((CGPoint, CGPoint) -> Void)?
|
||||
var updatePanEnded: (() -> Void)?
|
||||
|
||||
override var state: UIGestureRecognizer.State {
|
||||
didSet {
|
||||
@ -110,6 +112,8 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
|
||||
private var isTracking: Bool = false
|
||||
private var isValidated: Bool = false
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
@ -134,10 +138,33 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
|
||||
|
||||
if !self.isTracking {
|
||||
self.isTracking = true
|
||||
self.updateIsTracking?(touches.first?.location(in: self.view))
|
||||
self.initialLocation = touches.first?.location(in: self.view)
|
||||
self.updateIsTracking?(initialLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
if self.isValidated {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if let location = touches.first?.location(in: self.view), let initialLocation = self.initialLocation {
|
||||
self.updatePanMove?(initialLocation, CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.updatePanEnded?()
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.updatePanEnded?()
|
||||
}
|
||||
}
|
||||
|
||||
private final class StoryPinchGesture: UIPinchGestureRecognizer {
|
||||
@ -397,6 +424,9 @@ private final class StoryContainerScreenComponent: Component {
|
||||
private var isDisplayingInteractionGuide: Bool = false
|
||||
private var displayInteractionGuideDisposable: Disposable?
|
||||
|
||||
private var previousSeekTime: Double?
|
||||
private var initialSeekTimestamp: Double?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
|
||||
@ -468,6 +498,48 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
longPressRecognizer.updatePanMove = { [weak self] initialLocation, translation in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
|
||||
return
|
||||
}
|
||||
guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View else {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = CACurrentMediaTime()
|
||||
if let previousTime = self.previousSeekTime, currentTime - previousTime < 0.1 {
|
||||
return
|
||||
}
|
||||
self.previousSeekTime = currentTime
|
||||
|
||||
let initialSeekTimestamp: Double
|
||||
if let current = self.initialSeekTimestamp {
|
||||
initialSeekTimestamp = current
|
||||
} else {
|
||||
initialSeekTimestamp = visibleItemView.effectiveTimestamp
|
||||
self.initialSeekTimestamp = initialSeekTimestamp
|
||||
}
|
||||
|
||||
let timestamp: Double
|
||||
if translation.x > 0.0 {
|
||||
let fraction = translation.x / (self.bounds.width - initialLocation.x)
|
||||
timestamp = initialSeekTimestamp + (visibleItemView.effectiveDuration - initialSeekTimestamp) * fraction * fraction
|
||||
} else {
|
||||
let fraction = translation.x / initialLocation.x
|
||||
timestamp = initialSeekTimestamp + initialSeekTimestamp * fraction * fraction * -1.0
|
||||
}
|
||||
visibleItemView.seekTo(timestamp)
|
||||
}
|
||||
longPressRecognizer.updatePanEnded = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.initialSeekTimestamp = nil
|
||||
self.previousSeekTime = nil
|
||||
}
|
||||
longPressRecognizer.shouldBegin = { [weak self] touch in
|
||||
guard let self else {
|
||||
return false
|
||||
|
@ -397,6 +397,25 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var effectiveTimestamp: Double {
|
||||
guard let videoPlaybackStatus = self.videoPlaybackStatus else {
|
||||
return 0.0
|
||||
}
|
||||
return videoPlaybackStatus.timestamp
|
||||
}
|
||||
|
||||
var effectiveDuration: Double {
|
||||
let effectiveDuration: Double
|
||||
if let videoPlaybackStatus, videoPlaybackStatus.duration > 0.0 {
|
||||
effectiveDuration = videoPlaybackStatus.duration
|
||||
} else if case let .file(file) = self.currentMessageMedia, let duration = file.duration {
|
||||
effectiveDuration = Double(max(1, duration))
|
||||
} else {
|
||||
effectiveDuration = 1.0
|
||||
}
|
||||
return effectiveDuration
|
||||
}
|
||||
|
||||
private func updateVideoPlaybackProgress() {
|
||||
guard let videoPlaybackStatus = self.videoPlaybackStatus else {
|
||||
return
|
||||
@ -510,6 +529,14 @@ final class StoryItemContentComponent: Component {
|
||||
)
|
||||
}
|
||||
|
||||
func seekTo(_ timestamp: Double) {
|
||||
guard let videoNode = self.videoNode else {
|
||||
return
|
||||
}
|
||||
videoNode.seek(timestamp)
|
||||
self.updateVideoPlaybackProgress()
|
||||
}
|
||||
|
||||
func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StoryContentItem.Environment>, transition: Transition) -> CGSize {
|
||||
let previousItem = self.component?.item
|
||||
|
||||
|
@ -403,6 +403,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let itemsContainerView: UIView
|
||||
let controlsContainerView: UIView
|
||||
let controlsClippingView: UIView
|
||||
let controlsNavigationClippingView: UIView
|
||||
let topContentGradientView: UIImageView
|
||||
let bottomContentGradientLayer: SimpleGradientLayer
|
||||
let contentDimView: UIView
|
||||
@ -411,6 +412,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let closeButtonIconView: UIImageView
|
||||
|
||||
let navigationStrip = ComponentView<MediaNavigationStripComponent.EnvironmentType>()
|
||||
let seekLabel = ComponentView<Empty>()
|
||||
|
||||
var centerInfoItem: InfoItem?
|
||||
var leftInfoItem: InfoItem?
|
||||
@ -508,6 +510,12 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.controlsClippingView.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
self.controlsNavigationClippingView = SparseContainerView()
|
||||
self.controlsNavigationClippingView.clipsToBounds = true
|
||||
if #available(iOS 13.0, *) {
|
||||
self.controlsNavigationClippingView.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
self.topContentGradientView = UIImageView()
|
||||
if let image = StoryItemSetContainerComponent.shadowImage {
|
||||
self.topContentGradientView.image = image.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(image.size.height - 1.0))
|
||||
@ -539,11 +547,12 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer)
|
||||
|
||||
self.componentContainerView.addSubview(self.itemsContainerView)
|
||||
self.componentContainerView.addSubview(self.controlsNavigationClippingView)
|
||||
self.componentContainerView.addSubview(self.controlsClippingView)
|
||||
self.componentContainerView.addSubview(self.controlsContainerView)
|
||||
|
||||
self.controlsClippingView.addSubview(self.contentDimView)
|
||||
self.controlsClippingView.addSubview(self.topContentGradientView)
|
||||
self.controlsNavigationClippingView.addSubview(self.topContentGradientView)
|
||||
self.layer.addSublayer(self.bottomContentGradientLayer)
|
||||
|
||||
self.componentContainerView.addSubview(self.viewListsContainer)
|
||||
@ -2054,6 +2063,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
duration: 0.3
|
||||
)
|
||||
|
||||
self.controlsNavigationClippingView.layer.animatePosition(from: sourceLocalFrame.center, to: self.controlsNavigationClippingView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.controlsNavigationClippingView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.controlsNavigationClippingView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.storyItem.id]?.view.view {
|
||||
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width
|
||||
let innerFromFrame = CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: CGSize(width: innerSourceLocalFrame.width, height: visibleItemView.bounds.height * innerScale))
|
||||
@ -2264,6 +2276,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.controlsNavigationClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
|
||||
for transitionViewImpl in transitionViewsImpl {
|
||||
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)
|
||||
@ -2318,6 +2331,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
removeOnCompletion: false
|
||||
)
|
||||
|
||||
self.controlsNavigationClippingView.layer.animatePosition(from: self.controlsNavigationClippingView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.controlsNavigationClippingView.layer.animateBounds(from: self.controlsNavigationClippingView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
|
||||
self.overlayContainerView.clipsToBounds = true
|
||||
let overlayToFrame = sourceLocalFrame
|
||||
let overlayToBounds = CGRect(origin: CGPoint(x: overlayToFrame.minX, y: overlayToFrame.minY), size: overlayToFrame.size)
|
||||
@ -2382,6 +2398,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.controlsNavigationClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
|
||||
for transitionViewImpl in transitionViewsImpl {
|
||||
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)
|
||||
@ -2523,6 +2540,8 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
#endif*/
|
||||
}
|
||||
|
||||
let previousComponent = self.component
|
||||
|
||||
var isFirstItem = false
|
||||
var itemChanged = false
|
||||
var resetInputContents: MessageInputPanelComponent.SendMessageInput?
|
||||
@ -3603,6 +3622,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
transition.setPosition(view: self.controlsClippingView, position: contentFrame.center)
|
||||
transition.setBounds(view: self.controlsClippingView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||
|
||||
transition.setPosition(view: self.controlsNavigationClippingView, position: contentFrame.center)
|
||||
transition.setBounds(view: self.controlsNavigationClippingView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size))
|
||||
|
||||
var transform = CATransform3DMakeScale(contentVisualScale, contentVisualScale, 1.0)
|
||||
if let pinchState = component.pinchState {
|
||||
let pinchOffset = CGPoint(
|
||||
@ -3619,6 +3641,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
transition.setTransform(view: self.controlsContainerView, transform: transform)
|
||||
transition.setTransform(view: self.controlsClippingView, transform: transform)
|
||||
transition.setTransform(view: self.controlsNavigationClippingView, transform: transform)
|
||||
|
||||
transition.setCornerRadius(layer: self.controlsClippingView.layer, cornerRadius: 12.0 * (1.0 / contentVisualScale))
|
||||
|
||||
@ -3870,6 +3893,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
let controlsContainerAlpha = (component.hideUI || self.isEditingStory || self.viewListDisplayState != .hidden) ? 0.0 : 1.0
|
||||
transition.setAlpha(view: self.controlsContainerView, alpha: controlsContainerAlpha)
|
||||
transition.setAlpha(view: self.controlsClippingView, alpha: controlsContainerAlpha)
|
||||
transition.setAlpha(view: self.controlsNavigationClippingView, alpha: self.isEditingStory || self.viewListDisplayState != .hidden ? 0.0 : 1.0)
|
||||
|
||||
let focusedItem: StoryContentItem? = component.slice.item
|
||||
|
||||
@ -4584,7 +4608,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
//transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: inputPanelIsOverlay ? 1.0 : 0.0)
|
||||
transition.setAlpha(layer: self.bottomContentGradientLayer, alpha: 0.0)
|
||||
|
||||
var topGradientAlpha: CGFloat = (component.hideUI || self.viewListDisplayState != .hidden || self.isEditingStory) ? 0.0 : 1.0
|
||||
var topGradientAlpha: CGFloat = (self.viewListDisplayState != .hidden || self.isEditingStory) ? 0.0 : 1.0
|
||||
var normalDimAlpha: CGFloat = 0.0
|
||||
var forceDimAnimation = false
|
||||
if let captionItem = self.captionItem {
|
||||
@ -4644,10 +4668,9 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let startTime9 = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
let navigationStripSideInset: CGFloat = 8.0
|
||||
let navigationStripTopInset: CGFloat = 8.0
|
||||
if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id], let index = focusedItem.position {
|
||||
let navigationStripSideInset: CGFloat = 8.0
|
||||
let navigationStripTopInset: CGFloat = 8.0
|
||||
|
||||
var index = max(0, min(index, component.slice.totalCount - 1))
|
||||
var count = component.slice.totalCount
|
||||
if let dayCounters = focusedItem.dayCounters {
|
||||
@ -4655,11 +4678,18 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
count = dayCounters.totalCount
|
||||
}
|
||||
|
||||
let _ = self.navigationStrip.update(
|
||||
transition: transition,
|
||||
let isSeeking = component.isProgressPaused && component.hideUI
|
||||
|
||||
var navigationStripTransition = transition
|
||||
if let previousComponent, (previousComponent.isProgressPaused && component.hideUI) != isSeeking {
|
||||
navigationStripTransition = .easeInOut(duration: 0.3)
|
||||
}
|
||||
let navigationStripSize = self.navigationStrip.update(
|
||||
transition: navigationStripTransition,
|
||||
component: AnyComponent(MediaNavigationStripComponent(
|
||||
index: index,
|
||||
count: count
|
||||
count: count,
|
||||
isSeeking: isSeeking
|
||||
)),
|
||||
environment: {
|
||||
MediaNavigationStripComponent.EnvironmentType(
|
||||
@ -4667,16 +4697,34 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
currentIsBuffering: visibleItem.isBuffering
|
||||
)
|
||||
},
|
||||
containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)
|
||||
containerSize: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 6.0)
|
||||
)
|
||||
if let navigationStripView = self.navigationStrip.view {
|
||||
if navigationStripView.superview == nil {
|
||||
navigationStripView.isUserInteractionEnabled = false
|
||||
self.controlsClippingView.addSubview(navigationStripView)
|
||||
self.controlsNavigationClippingView.addSubview(navigationStripView)
|
||||
}
|
||||
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0)))
|
||||
transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: navigationStripSize.height)))
|
||||
transition.setAlpha(view: navigationStripView, alpha: self.isEditingStory ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let seekLabelSize = self.seekLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: "Slide left or right to seek", font: Font.semibold(14.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let seekLabelView = self.seekLabel.view {
|
||||
if seekLabelView.superview == nil {
|
||||
seekLabelView.alpha = 0.0
|
||||
seekLabelView.isUserInteractionEnabled = false
|
||||
self.controlsNavigationClippingView.addSubview(seekLabelView)
|
||||
}
|
||||
seekLabelView.bounds = CGRect(origin: .zero, size: seekLabelSize)
|
||||
navigationStripTransition.setPosition(view: seekLabelView, position: CGPoint(x: availableSize.width / 2.0, y: navigationStripTopInset + 22.0 + 6.0 - (!isSeeking ? 12.0 : 0.0)))
|
||||
navigationStripTransition.setAlpha(view: seekLabelView, alpha: isSeeking ? 1.0 : 0.0)
|
||||
navigationStripTransition.setScale(view: seekLabelView, scale: isSeeking ? 1.0 : 0.02)
|
||||
}
|
||||
}
|
||||
|
||||
component.externalState.derivedMediaSize = contentFrame.size
|
||||
|
Loading…
x
Reference in New Issue
Block a user