GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,155 @@
// MARK: Swiftgram reorderable row for tab organizer and hidden settings tabs
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
public final class ItemListReorderableRowItem: ListViewItem, ItemListItem {
public let presentationData: ItemListPresentationData
public let title: String
public let iconName: String?
public let sectionId: ItemListSectionId
public let reorderId: AnyHashable
public init(presentationData: ItemListPresentationData, title: String, iconName: String? = nil, sectionId: ItemListSectionId, reorderId: AnyHashable) {
self.presentationData = presentationData
self.title = title
self.iconName = iconName
self.sectionId = sectionId
self.reorderId = reorderId
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ItemListReorderableRowItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, { return (nil, { _ in apply() }) })
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? ItemListReorderableRowItemNode else { return }
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in apply() })
}
}
}
}
public var selectable: Bool { false }
}
public final class ItemListReorderableRowItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let titleNode: TextNode
private let iconNode: ASImageNode
private var reorderControlNode: ItemListEditableReorderControlNode?
private var item: ItemListReorderableRowItem?
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.contentMode = .scaleAspectFit
self.iconNode.isUserInteractionEnabled = false
super.init(layerBacked: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.topStripeNode)
self.addSubnode(self.bottomStripeNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
}
public func asyncLayout() -> (_ item: ItemListReorderableRowItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
return { [weak self] item, params, neighbors in
let iconSize: CGFloat = 22.0
let iconGap: CGFloat = 12.0
let baseLeftInset: CGFloat = 16.0 + params.leftInset
let hasIcon = item.iconName != nil
let leftInset: CGFloat = hasIcon ? baseLeftInset + iconSize + iconGap : baseLeftInset
let reorderInset: CGFloat = 40.0
let verticalInset: CGFloat = 13.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: Font.regular(17), textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - reorderInset - params.rightInset - 16.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: .zero))
let (reorderWidth, reorderApplyClosure) = reorderControlLayout(item.presentationData.theme)
let contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let hasCorners = itemListHasRoundedBlockLayout(params)
return (layout, {
if let strongSelf = self {
strongSelf.item = item
let theme = item.presentationData.theme
strongSelf.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
strongSelf.highlightedBackgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor
let reorderNode = reorderApplyClosure(layoutSize.height, false, .immediate)
if strongSelf.reorderControlNode !== reorderNode {
strongSelf.reorderControlNode?.removeFromSupernode()
strongSelf.reorderControlNode = reorderNode
strongSelf.addSubnode(reorderNode)
}
reorderNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - reorderWidth, y: 0), size: CGSize(width: reorderWidth, height: layoutSize.height))
_ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
// Icon
if let iconName = item.iconName {
let config = UIImage.SymbolConfiguration(pointSize: iconSize * 0.82, weight: .medium)
let img = UIImage(systemName: iconName, withConfiguration: config)?
.withTintColor(theme.list.itemAccentColor, renderingMode: .alwaysOriginal)
strongSelf.iconNode.image = img
let iconY = (layoutSize.height - iconSize) / 2.0
strongSelf.iconNode.frame = CGRect(x: baseLeftInset, y: iconY, width: iconSize, height: iconSize)
strongSelf.iconNode.isHidden = false
} else {
strongSelf.iconNode.isHidden = true
}
strongSelf.topStripeNode.isHidden = hasCorners
strongSelf.bottomStripeNode.isHidden = hasCorners
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: layoutSize.width, height: UIScreenPixel))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0, y: layoutSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width, height: UIScreenPixel))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: layoutSize)
}
})
}
}
override public func isReorderable(at point: CGPoint) -> Bool {
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) {
return true
}
return false
}
}
@@ -0,0 +1,415 @@
// MARK: Swiftgram
import SGLogging
import SGSimpleSettings
import SGStrings
import SGAPIToken
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import MtProtoKit
import MessageUI
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import AccountContext
import AppBundle
import WebKit
import PeerNameColorScreen
public class SGItemListCounter {
private var _count = 0
public init() {}
public var count: Int {
_count += 1
return _count
}
public func increment(_ amount: Int) {
_count += amount
}
public func countWith(_ amount: Int) -> Int {
_count += amount
return count
}
}
public protocol SGItemListSection: Equatable {
var rawValue: Int32 { get }
}
public final class SGItemListArguments<BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable> {
let context: AccountContext
//
let setBoolValue: (BoolSetting, Bool) -> Void
let updateSliderValue: (SliderSetting, Int32) -> Void
let setOneFromManyValue: (OneFromManySetting) -> Void
let openDisclosureLink: (DisclosureLink) -> Void
let action: (ActionType) -> Void
let searchInput: (String) -> Void
/// Resolves plugin icon ref (e.g. "glePlugins/1") to UIImage for toggleWithIcon rows.
let iconResolver: ((String?) -> UIImage?)?
public init(
context: AccountContext,
//
setBoolValue: @escaping (BoolSetting, Bool) -> Void = { _,_ in },
updateSliderValue: @escaping (SliderSetting, Int32) -> Void = { _,_ in },
setOneFromManyValue: @escaping (OneFromManySetting) -> Void = { _ in },
openDisclosureLink: @escaping (DisclosureLink) -> Void = { _ in},
action: @escaping (ActionType) -> Void = { _ in },
searchInput: @escaping (String) -> Void = { _ in },
iconResolver: ((String?) -> UIImage?)? = nil
) {
self.context = context
self.setBoolValue = setBoolValue
self.updateSliderValue = updateSliderValue
self.setOneFromManyValue = setOneFromManyValue
self.openDisclosureLink = openDisclosureLink
self.action = action
self.searchInput = searchInput
self.iconResolver = iconResolver
}
}
public enum SGItemListUIEntry<Section: SGItemListSection, BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable>: ItemListNodeEntry {
case header(id: Int, section: Section, text: String, badge: String?)
case toggle(id: Int, section: Section, settingName: BoolSetting, value: Bool, text: String, enabled: Bool)
case toggleWithIcon(id: Int, section: Section, settingName: BoolSetting, value: Bool, text: String, enabled: Bool, iconRef: String?)
case notice(id: Int, section: Section, text: String)
case percentageSlider(id: Int, section: Section, settingName: SliderSetting, value: Int32)
case delaySecondsSlider(id: Int, section: Section, settingName: SliderSetting, value: Int32, leftLabel: String, rightLabel: String, centerLabels: [String])
case fontSizeMultiplierSlider(id: Int, section: Section, settingName: SliderSetting, value: Int32)
case oneFromManySelector(id: Int, section: Section, settingName: OneFromManySetting, text: String, value: String, enabled: Bool)
case disclosure(id: Int, section: Section, link: DisclosureLink, text: String)
/// Disclosure row with optional icon (e.g. for GLEGram tab buttons).
case disclosureWithIcon(id: Int, section: Section, link: DisclosureLink, text: String, iconRef: String)
case peerColorDisclosurePreview(id: Int, section: Section, name: String, color: UIColor)
case action(id: Int, section: Section, actionType: ActionType, text: String, kind: ItemListActionKind)
case searchInput(id: Int, section: Section, title: NSAttributedString, text: String, placeholder: String)
case reorderableRow(id: Int, section: Section, text: String, reorderId: AnyHashable, iconName: String?)
public var section: ItemListSectionId {
switch self {
case let .header(_, sectionId, _, _):
return sectionId.rawValue
case let .toggle(_, sectionId, _, _, _, _):
return sectionId.rawValue
case let .toggleWithIcon(_, sectionId, _, _, _, _, _):
return sectionId.rawValue
case let .notice(_, sectionId, _):
return sectionId.rawValue
case let .disclosure(_, sectionId, _, _):
return sectionId.rawValue
case let .disclosureWithIcon(_, sectionId, _, _, _):
return sectionId.rawValue
case let .percentageSlider(_, sectionId, _, _):
return sectionId.rawValue
case let .delaySecondsSlider(_, sectionId, _, _, _, _, _):
return sectionId.rawValue
case let .fontSizeMultiplierSlider(_, sectionId, _, _):
return sectionId.rawValue
case let .peerColorDisclosurePreview(_, sectionId, _, _):
return sectionId.rawValue
case let .oneFromManySelector(_, sectionId, _, _, _, _):
return sectionId.rawValue
case let .action(_, sectionId, _, _, _):
return sectionId.rawValue
case let .searchInput(_, sectionId, _, _, _):
return sectionId.rawValue
case let .reorderableRow(_, sectionId, _, _, _):
return sectionId.rawValue
}
}
public var stableId: Int {
switch self {
case let .header(stableIdValue, _, _, _):
return stableIdValue
case let .toggle(stableIdValue, _, _, _, _, _):
return stableIdValue
case let .toggleWithIcon(stableIdValue, _, _, _, _, _, _):
return stableIdValue
case let .notice(stableIdValue, _, _):
return stableIdValue
case let .disclosure(stableIdValue, _, _, _):
return stableIdValue
case let .disclosureWithIcon(stableIdValue, _, _, _, _):
return stableIdValue
case let .percentageSlider(stableIdValue, _, _, _):
return stableIdValue
case let .delaySecondsSlider(stableIdValue, _, _, _, _, _, _):
return stableIdValue
case let .fontSizeMultiplierSlider(stableIdValue, _, _, _):
return stableIdValue
case let .peerColorDisclosurePreview(stableIdValue, _, _, _):
return stableIdValue
case let .oneFromManySelector(stableIdValue, _, _, _, _, _):
return stableIdValue
case let .action(stableIdValue, _, _, _, _):
return stableIdValue
case let .searchInput(stableIdValue, _, _, _, _):
return stableIdValue
case let .reorderableRow(stableIdValue, _, _, _, _):
return stableIdValue
}
}
public static func <(lhs: SGItemListUIEntry, rhs: SGItemListUIEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
public static func ==(lhs: SGItemListUIEntry, rhs: SGItemListUIEntry) -> Bool {
switch (lhs, rhs) {
case let (.header(id1, section1, text1, badge1), .header(id2, section2, text2, badge2)):
return id1 == id2 && section1 == section2 && text1 == text2 && badge1 == badge2
case let (.toggle(id1, section1, settingName1, value1, text1, enabled1), .toggle(id2, section2, settingName2, value2, text2, enabled2)):
return id1 == id2 && section1 == section2 && settingName1 == settingName2 && value1 == value2 && text1 == text2 && enabled1 == enabled2
case let (.toggleWithIcon(id1, section1, settingName1, value1, text1, enabled1, iconRef1), .toggleWithIcon(id2, section2, settingName2, value2, text2, enabled2, iconRef2)):
return id1 == id2 && section1 == section2 && settingName1 == settingName2 && value1 == value2 && text1 == text2 && enabled1 == enabled2 && iconRef1 == iconRef2
case let (.notice(id1, section1, text1), .notice(id2, section2, text2)):
return id1 == id2 && section1 == section2 && text1 == text2
case let (.percentageSlider(id1, section1, settingName1, value1), .percentageSlider(id2, section2, settingName2, value2)):
return id1 == id2 && section1 == section2 && value1 == value2 && settingName1 == settingName2
case let (.delaySecondsSlider(id1, section1, settingName1, value1, l1, r1, c1), .delaySecondsSlider(id2, section2, settingName2, value2, l2, r2, c2)):
return id1 == id2 && section1 == section2 && settingName1 == settingName2 && value1 == value2 && l1 == l2 && r1 == r2 && c1 == c2
case let (.fontSizeMultiplierSlider(id1, section1, settingName1, value1), .fontSizeMultiplierSlider(id2, section2, settingName2, value2)):
return id1 == id2 && section1 == section2 && settingName1 == settingName2 && value1 == value2
case let (.disclosure(id1, section1, link1, text1), .disclosure(id2, section2, link2, text2)):
return id1 == id2 && section1 == section2 && link1 == link2 && text1 == text2
case let (.disclosureWithIcon(id1, section1, link1, text1, iconRef1), .disclosureWithIcon(id2, section2, link2, text2, iconRef2)):
return id1 == id2 && section1 == section2 && link1 == link2 && text1 == text2 && iconRef1 == iconRef2
case let (.peerColorDisclosurePreview(id1, section1, name1, currentColor1), .peerColorDisclosurePreview(id2, section2, name2, currentColor2)):
return id1 == id2 && section1 == section2 && name1 == name2 && currentColor1 == currentColor2
case let (.oneFromManySelector(id1, section1, settingName1, text1, value1, enabled1), .oneFromManySelector(id2, section2, settingName2, text2, value2, enabled2)):
return id1 == id2 && section1 == section2 && settingName1 == settingName2 && text1 == text2 && value1 == value2 && enabled1 == enabled2
case let (.action(id1, section1, actionType1, text1, kind1), .action(id2, section2, actionType2, text2, kind2)):
return id1 == id2 && section1 == section2 && actionType1 == actionType2 && text1 == text2 && kind1 == kind2
case let (.searchInput(id1, lhsValue1, lhsValue2, lhsValue3, lhsValue4), .searchInput(id2, rhsValue1, rhsValue2, rhsValue3, rhsValue4)):
return id1 == id2 && lhsValue1 == rhsValue1 && lhsValue2 == rhsValue2 && lhsValue3 == rhsValue3 && lhsValue4 == rhsValue4
case let (.reorderableRow(id1, section1, text1, reorderId1, icon1), .reorderableRow(id2, section2, text2, reorderId2, icon2)):
return id1 == id2 && section1 == section2 && text1 == text2 && reorderId1 == reorderId2 && icon1 == icon2
default:
return false
}
}
public func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! SGItemListArguments<BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>
switch self {
case let .header(_, _, string, badge):
return ItemListSectionHeaderItem(presentationData: presentationData, text: string, badge: badge, sectionId: self.section)
case let .toggle(_, _, setting, value, text, enabled):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.setBoolValue(setting, value)
})
case let .toggleWithIcon(_, _, setting, value, text, enabled, iconRef):
let icon = arguments.iconResolver?(iconRef)
return ItemListSwitchItem(presentationData: presentationData, icon: icon, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
arguments.setBoolValue(setting, value)
})
case let .notice(_, _, string):
return ItemListTextItem(presentationData: presentationData, text: .markdown(string), sectionId: self.section)
case let .disclosure(_, _, link, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks) {
arguments.openDisclosureLink(link)
}
case let .disclosureWithIcon(_, _, link, text, iconRef):
let icon = arguments.iconResolver?(iconRef)
return ItemListDisclosureItem(presentationData: presentationData, icon: icon, title: text, label: "", sectionId: self.section, style: .blocks) {
arguments.openDisclosureLink(link)
}
case let .percentageSlider(_, _, setting, value):
return SliderPercentageItem(
theme: presentationData.theme,
strings: presentationData.strings,
value: value,
sectionId: self.section,
updated: { value in
arguments.updateSliderValue(setting, value)
}
)
case let .delaySecondsSlider(_, _, setting, value, leftLabel, rightLabel, centerLabels):
return SliderDelaySecondsItem(
theme: presentationData.theme,
strings: presentationData.strings,
value: value,
leftLabel: leftLabel,
rightLabel: rightLabel,
centerLabels: centerLabels,
sectionId: self.section,
updated: { value in
arguments.updateSliderValue(setting, value)
}
)
case let .fontSizeMultiplierSlider(_, _, setting, value):
return SliderFontSizeMultiplierItem(
theme: presentationData.theme,
strings: presentationData.strings,
value: value,
sectionId: self.section,
updated: { value in
arguments.updateSliderValue(setting, value)
}
)
case let .peerColorDisclosurePreview(_, _, name, color):
return ItemListDisclosureItem(presentationData: presentationData, title: " ", enabled: false, label: name, labelStyle: .semitransparentBadge(color), centerLabelAlignment: true, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
})
case let .oneFromManySelector(_, _, settingName, text, value, enabled):
return ItemListDisclosureItem(presentationData: presentationData, title: text, enabled: enabled, label: value, sectionId: self.section, style: .blocks, action: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) // Closing search keyboard if active
arguments.setOneFromManyValue(settingName)
})
case let .action(_, _, actionType, text, kind):
return ItemListActionItem(presentationData: presentationData, title: text, kind: kind, alignment: .natural, sectionId: self.section, style: .blocks, action: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) // Closing search keyboard if active
arguments.action(actionType)
})
case let .searchInput(_, _, title, text, placeholder):
return ItemListSingleLineInputItem(presentationData: presentationData, title: title, text: text, placeholder: placeholder, returnKeyType: .done, spacing: 3.0, clearType: .always, selectAllOnFocus: true, secondaryStyle: true, sectionId: self.section, textUpdated: { input in arguments.searchInput(input) }, action: {}, dismissKeyboardOnEnter: true)
case let .reorderableRow(_, _, text, reorderId, iconName):
return ItemListReorderableRowItem(presentationData: presentationData, title: text, iconName: iconName, sectionId: self.section, reorderId: reorderId)
}
}
}
public func filterSGItemListUIEntrires<Section: SGItemListSection & Hashable, BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable>(
entries: [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>],
by searchQuery: String?
) -> [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>] {
guard let query = searchQuery?.lowercased(), !query.isEmpty else {
return entries
}
var sectionIdsForEntireIncludion: Set<ItemListSectionId> = []
var sectionIdsWithMatches: Set<ItemListSectionId> = []
var filteredEntries: [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>] = []
func entryMatches(_ entry: SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>, query: String) -> Bool {
switch entry {
case .header(_, _, let text, _):
return text.lowercased().contains(query)
case .toggle(_, _, _, _, let text, _):
return text.lowercased().contains(query)
case .toggleWithIcon(_, _, _, _, let text, _, _):
return text.lowercased().contains(query)
case .notice(_, _, let text):
return text.lowercased().contains(query)
case .percentageSlider:
return false
case .delaySecondsSlider(_, _, _, _, let leftLabel, let rightLabel, let centerLabels):
return (centerLabels + [leftLabel, rightLabel]).contains { $0.lowercased().contains(query) }
case .fontSizeMultiplierSlider:
return false
case .oneFromManySelector(_, _, _, let text, let value, _):
return text.lowercased().contains(query) || value.lowercased().contains(query)
case .disclosure(_, _, _, let text):
return text.lowercased().contains(query)
case .disclosureWithIcon(_, _, _, let text, _):
return text.lowercased().contains(query)
case .peerColorDisclosurePreview:
return false // Never indexed during search
case .action(_, _, _, let text, _):
return text.lowercased().contains(query)
case .searchInput:
return true // Never hiding search input
case .reorderableRow(_, _, let text, _, _):
return text.lowercased().contains(query)
}
}
// First pass: identify sections with matches
for entry in entries {
if entryMatches(entry, query: query) {
switch entry {
case .searchInput:
continue
default:
sectionIdsWithMatches.insert(entry.section)
}
}
}
// Second pass: keep matching entries and headers of sections with matches
for (index, entry) in entries.enumerated() {
switch entry {
case .header:
if entryMatches(entry, query: query) {
// Will show all entries for the same section
sectionIdsForEntireIncludion.insert(entry.section)
if !filteredEntries.contains(entry) {
filteredEntries.append(entry)
}
}
// Or show header if something from the section already matched
if sectionIdsWithMatches.contains(entry.section) {
if !filteredEntries.contains(entry) {
filteredEntries.append(entry)
}
}
default:
if entryMatches(entry, query: query) {
if case .notice = entry {
// add previous entry to if it's not another notice and if it's not already here
// possibly targeting related toggle / setting if we've matched it's description (notice) in search
if index > 0 {
let previousEntry = entries[index - 1]
if case .notice = previousEntry {} else {
if !filteredEntries.contains(previousEntry) {
filteredEntries.append(previousEntry)
}
}
}
if !filteredEntries.contains(entry) {
filteredEntries.append(entry)
}
} else {
if !filteredEntries.contains(entry) {
filteredEntries.append(entry)
}
// add next entry if it's notice
// possibly targeting description (notice) for the currently search-matched toggle/setting
if index < entries.count - 1 {
let nextEntry = entries[index + 1]
if case .notice = nextEntry {
if !filteredEntries.contains(nextEntry) {
filteredEntries.append(nextEntry)
}
}
}
}
} else if sectionIdsForEntireIncludion.contains(entry.section) {
if !filteredEntries.contains(entry) {
filteredEntries.append(entry)
}
}
}
}
return filteredEntries
}
@@ -0,0 +1,742 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import AppBundle
public class SliderPercentageItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
public let sectionId: ItemListSectionId
let updated: (Int32) -> Void
public init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.sectionId = sectionId
self.updated = updated
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = SliderPercentageItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? SliderPercentageItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func rescalePercentageValueToSlider(_ value: CGFloat) -> CGFloat {
return max(0.0, min(1.0, value))
}
private func rescaleSliderValueToPercentageValue(_ value: CGFloat) -> CGFloat {
return max(0.0, min(1.0, value))
}
class SliderPercentageItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let leftTextNode: ImmediateTextNode
private let rightTextNode: ImmediateTextNode
private let centerTextNode: ImmediateTextNode
private let centerMeasureTextNode: ImmediateTextNode
private let batteryImage: UIImage?
private let batteryBackgroundNode: ASImageNode
private let batteryForegroundNode: ASImageNode
private var item: SliderPercentageItem?
private var layoutParams: ListViewItemLayoutParams?
// MARK: Swiftgram
private let activateArea: AccessibilityAreaNode
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftTextNode = ImmediateTextNode()
self.rightTextNode = ImmediateTextNode()
self.centerTextNode = ImmediateTextNode()
self.centerMeasureTextNode = ImmediateTextNode()
self.batteryImage = nil //UIImage(bundleImageName: "Settings/UsageBatteryFrame")
self.batteryBackgroundNode = ASImageNode()
self.batteryForegroundNode = ASImageNode()
// MARK: Swiftgram
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false)
self.addSubnode(self.leftTextNode)
self.addSubnode(self.rightTextNode)
self.addSubnode(self.centerTextNode)
self.addSubnode(self.batteryBackgroundNode)
self.addSubnode(self.batteryForegroundNode)
self.addSubnode(self.activateArea)
// MARK: Swiftgram
self.activateArea.increment = { [weak self] in
if let self {
self.sliderView?.increase(by: 0.10)
}
}
self.activateArea.decrement = { [weak self] in
if let self {
self.sliderView?.decrease(by: 0.10)
}
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enableEdgeTap = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 4.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.maximumValue = 1.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.displayEdges = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = rescalePercentageValueToSlider(CGFloat(item.value) / 100.0)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: SliderPercentageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "0%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "100%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
let centralText: String = "\(item.value)%"
let centralMeasureText: String = centralText
strongSelf.batteryBackgroundNode.isHidden = true
strongSelf.batteryForegroundNode.isHidden = strongSelf.batteryBackgroundNode.isHidden
strongSelf.centerTextNode.attributedText = NSAttributedString(string: centralText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
strongSelf.centerMeasureTextNode.attributedText = NSAttributedString(string: centralMeasureText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
strongSelf.leftTextNode.isAccessibilityElement = true
strongSelf.leftTextNode.accessibilityLabel = "Minimum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.minimumValue ?? 0.0) * 100.0))%"
strongSelf.rightTextNode.isAccessibilityElement = true
strongSelf.rightTextNode.accessibilityLabel = "Maximum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.maximumValue ?? 1.0) * 100.0))%"
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let centerMeasureTextSize = strongSelf.centerMeasureTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let sideInset: CGFloat = 18.0
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize)
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize)
var centerFrame = CGRect(origin: CGPoint(x: floor((params.width - centerMeasureTextSize.width) / 2.0), y: 11.0), size: centerTextSize)
if !strongSelf.batteryBackgroundNode.isHidden {
centerFrame.origin.x -= 12.0
}
strongSelf.centerTextNode.frame = centerFrame
if let frameImage = strongSelf.batteryImage {
strongSelf.batteryBackgroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: frameImage, color: item.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.9)) {
image.draw(in: CGRect(origin: CGPoint(), size: size))
let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0))
context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath)
context.clip()
}
UIGraphicsPopContext()
})
strongSelf.batteryForegroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0))
context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath)
context.clip()
context.setFillColor(UIColor.white.cgColor)
context.addPath(UIBezierPath(roundedRect: CGRect(origin: contentRect.origin, size: CGSize(width: contentRect.width * CGFloat(item.value) / 100.0, height: contentRect.height)), cornerRadius: 1.0).cgPath)
context.fillPath()
UIGraphicsPopContext()
})
let batteryColor: UIColor
if item.value <= 20 {
batteryColor = UIColor(rgb: 0xFF3B30)
} else {
batteryColor = item.theme.list.itemSwitchColors.positiveColor
}
if strongSelf.batteryForegroundNode.layer.layerTintColor == nil {
strongSelf.batteryForegroundNode.layer.layerTintColor = batteryColor.cgColor
} else {
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTintColor(layer: strongSelf.batteryForegroundNode.layer, color: batteryColor)
}
strongSelf.batteryBackgroundNode.frame = CGRect(origin: CGPoint(x: centerFrame.minX + centerMeasureTextSize.width + 4.0, y: floor(centerFrame.midY - frameImage.size.height * 0.5)), size: frameImage.size)
strongSelf.batteryForegroundNode.frame = strongSelf.batteryBackgroundNode.frame
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSecondaryTextColor
sliderView.trackColor = item.theme.list.itemAccentColor.withAlphaComponent(0.45)
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
}
strongSelf.activateArea.accessibilityLabel = "Slider"
strongSelf.activateArea.accessibilityValue = centralMeasureText
strongSelf.activateArea.accessibilityTraits = .adjustable
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
self.item?.updated(Int32(rescaleSliderValueToPercentageValue(sliderView.value) * 100.0))
}
}
// MARK: - SliderDelaySecondsItem (Ghost mode: message send delay 0 / 12 / 30 / 45 sec)
private let kDelaySecondsValues: [Int32] = [0, 12, 30, 45]
private func indexForDelaySeconds(_ value: Int32) -> Int {
guard let idx = kDelaySecondsValues.firstIndex(of: value) else { return 0 }
return idx
}
private func delaySecondsForIndex(_ index: Int) -> Int32 {
let i = max(0, min(index, kDelaySecondsValues.count - 1))
return kDelaySecondsValues[i]
}
public class SliderDelaySecondsItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let leftLabel: String
let rightLabel: String
let centerLabels: [String]
public let sectionId: ItemListSectionId
let updated: (Int32) -> Void
public init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, leftLabel: String, rightLabel: String, centerLabels: [String], sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.leftLabel = leftLabel
self.rightLabel = rightLabel
self.centerLabels = centerLabels
self.sectionId = sectionId
self.updated = updated
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = SliderDelaySecondsItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, { return (nil, { _ in apply() }) })
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? SliderDelaySecondsItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in apply() })
}
}
}
}
}
}
class SliderDelaySecondsItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let leftTextNode: ImmediateTextNode
private let rightTextNode: ImmediateTextNode
private let centerTextNode: ImmediateTextNode
private let centerMeasureTextNode: ImmediateTextNode
private var item: SliderDelaySecondsItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftTextNode = ImmediateTextNode()
self.rightTextNode = ImmediateTextNode()
self.centerTextNode = ImmediateTextNode()
self.centerMeasureTextNode = ImmediateTextNode()
super.init(layerBacked: false)
self.addSubnode(self.leftTextNode)
self.addSubnode(self.rightTextNode)
self.addSubnode(self.centerTextNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enableEdgeTap = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 4.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = CGFloat(kDelaySecondsValues.count - 1)
sliderView.startValue = 0.0
sliderView.displayEdges = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.positionsCount = kDelaySecondsValues.count
sliderView.disableSnapToPositions = false
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(indexForDelaySeconds(item.value))
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: SliderDelaySecondsItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme { themeUpdated = true }
let contentSize = CGSize(width: params.width, height: 88.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let separatorHeight = UIScreenPixel
return (layout, { [weak self] in
guard let strongSelf = self else { return }
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false): strongSelf.topStripeNode.isHidden = true
default: hasTopCorners = true; strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
strongSelf.leftTextNode.attributedText = NSAttributedString(string: item.leftLabel, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
strongSelf.rightTextNode.attributedText = NSAttributedString(string: item.rightLabel, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
let idx = indexForDelaySeconds(item.value)
let centerLabel = idx < item.centerLabels.count ? item.centerLabels[idx] : item.centerLabels[0]
strongSelf.centerTextNode.attributedText = NSAttributedString(string: centerLabel, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
strongSelf.centerMeasureTextNode.attributedText = NSAttributedString(string: centerLabel, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
let sideInset: CGFloat = 18.0
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
let centerMeasureTextSize = strongSelf.centerMeasureTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize)
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize)
let centerFrame = CGRect(origin: CGPoint(x: floor((params.width - centerMeasureTextSize.width) / 2.0), y: 11.0), size: centerTextSize)
strongSelf.centerTextNode.frame = centerFrame
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.value = CGFloat(indexForDelaySeconds(item.value))
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
}
})
}
}
@objc private func sliderValueChanged() {
guard let sliderView = self.sliderView, let item = self.item else { return }
let idx = Int(round(sliderView.value))
item.updated(delaySecondsForIndex(idx))
}
}
// MARK: - SliderFontSizeMultiplierItem (50150%)
public class SliderFontSizeMultiplierItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
public let sectionId: ItemListSectionId
let updated: (Int32) -> Void
public init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.value = max(50, min(150, value))
self.sectionId = sectionId
self.updated = updated
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = SliderFontSizeMultiplierItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async { completion(node, { return (nil, { _ in apply() }) }) }
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? SliderFontSizeMultiplierItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async { completion(layout, { _ in apply() }) }
}
}
}
}
}
// 50, 55, 60, ..., 150 21 дискретных позиций, как у SliderDelaySecondsItem
private let kFontSizeMultiplierValues: [Int32] = (10...30).map { Int32($0 * 5) } // 50..150 step 5
private func indexForFontSizeMultiplier(_ value: Int32) -> Int {
let v = max(50, min(150, value))
return max(0, min(kFontSizeMultiplierValues.count - 1, Int((v - 50) / 5)))
}
private func fontSizeMultiplierForIndex(_ index: Int) -> Int32 {
let i = max(0, min(index, kFontSizeMultiplierValues.count - 1))
return kFontSizeMultiplierValues[i]
}
class SliderFontSizeMultiplierItemNode: ListViewItemNode {
private let backgroundNode = ASDisplayNode()
private let topStripeNode = ASDisplayNode()
private let bottomStripeNode = ASDisplayNode()
private let maskNode = ASImageNode()
private var sliderView: TGPhotoEditorSliderView?
private let leftTextNode = ImmediateTextNode()
private let rightTextNode = ImmediateTextNode()
private let centerTextNode = ImmediateTextNode()
private var item: SliderFontSizeMultiplierItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
super.init(layerBacked: false)
backgroundNode.isLayerBacked = true
topStripeNode.isLayerBacked = true
bottomStripeNode.isLayerBacked = true
addSubnode(leftTextNode)
addSubnode(rightTextNode)
addSubnode(centerTextNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enableEdgeTap = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 4.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = CGFloat(kFontSizeMultiplierValues.count - 1)
sliderView.startValue = 0.0
sliderView.displayEdges = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.positionsCount = kFontSizeMultiplierValues.count
sliderView.disableSnapToPositions = false
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(indexForFontSizeMultiplier(item.value))
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
}
view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: SliderFontSizeMultiplierItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
let themeUpdated = currentItem?.theme !== item.theme
let contentSize = CGSize(width: params.width, height: 88.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let separatorHeight = UIScreenPixel
return (layout, { [weak self] in
guard let strongSelf = self else { return }
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false, hasBottomCorners = false
switch neighbors.top {
case .sameSection(false): strongSelf.topStripeNode.isHidden = true
default: hasTopCorners = true; strongSelf.topStripeNode.isHidden = hasCorners
}
var bottomStripeInset: CGFloat = 0, bottomStripeOffset: CGFloat = 0
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "50%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "150%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
let displayValue = fontSizeMultiplierForIndex(indexForFontSizeMultiplier(item.value))
let centerLabel = "\(displayValue)%"
strongSelf.centerTextNode.attributedText = NSAttributedString(string: centerLabel, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
let sideInset: CGFloat = 18.0
let leftSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100, height: 100))
let rightSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100, height: 100))
let centerSize = strongSelf.centerTextNode.updateLayout(CGSize(width: 200, height: 100))
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15), size: leftSize)
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightSize.width, y: 15), size: rightSize)
strongSelf.centerTextNode.frame = CGRect(origin: CGPoint(x: floor((params.width - centerSize.width) / 2), y: 11), size: centerSize)
if let sv = strongSelf.sliderView {
if themeUpdated {
sv.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sv.backColor = item.theme.list.itemSwitchColors.frameColor
sv.trackColor = item.theme.list.itemAccentColor
sv.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sv.value = CGFloat(indexForFontSizeMultiplier(item.value))
sv.frame = CGRect(origin: CGPoint(x: params.leftInset + 18, y: 36), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44))
}
})
}
}
@objc private func sliderValueChanged() {
guard let sliderView = self.sliderView, let item = self.item else { return }
let idx = Int(round(sliderView.value))
let v = fontSizeMultiplierForIndex(idx)
// Обновляем подпись локально, без reload списка
centerTextNode.attributedText = NSAttributedString(string: "\(v)%", font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
let centerSize = centerTextNode.updateLayout(CGSize(width: 200, height: 100))
if let params = layoutParams {
centerTextNode.frame = CGRect(origin: CGPoint(x: floor((params.width - centerSize.width) / 2), y: 11), size: centerSize)
}
item.updated(v)
}
}