Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,51 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import LegacyComponents
import TelegramUIPreferences
import MediaResources
import AccountContext
import LegacyUI
import LegacyMediaPickerUI
import LegacyComponents
import LocalMediaResources
import ImageBlur
import WallpaperGridScreen
import WallpaperGalleryScreen
func presentCustomWallpaperPicker(context: AccountContext, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = legacyWallpaperPicker(context: context, presentationData: presentationData).start(next: { generator in
let legacyController = LegacyController(presentation: .modal(animateIn: true), theme: presentationData.theme)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
let controller = generator(legacyController.context)
legacyController.bind(controller: controller)
legacyController.deferScreenEdgeGestures = [.top]
controller.selectionBlock = { [weak legacyController] asset, _ in
if let asset = asset {
let controller = WallpaperGalleryController(context: context, source: .asset(asset.backingAsset))
controller.apply = { [weak legacyController, weak controller] wallpaper, mode, editedImage, cropRect, brightness, _ in
if let legacyController = legacyController, let controller = controller {
uploadCustomWallpaper(context: context, wallpaper: wallpaper, mode: mode, editedImage: nil, cropRect: cropRect, brightness: brightness, completion: { [weak legacyController, weak controller] in
if let legacyController = legacyController, let controller = controller {
legacyController.dismiss()
controller.dismiss(forceAway: true)
}
})
}
}
push(controller)
}
}
controller.dismissalBlock = { [weak legacyController] in
if let legacyController = legacyController {
legacyController.dismiss()
}
}
present(legacyController)
})
}
@@ -0,0 +1,762 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AlertUI
import PresentationDataUtils
import LegacyMediaPickerUI
import WallpaperResources
import AccountContext
import MediaResources
import ThemeAccentColorScreen
import GenerateThemeName
private final class EditThemeControllerArguments {
let context: AccountContext
let updateState: ((EditThemeControllerState) -> EditThemeControllerState) -> Void
let openColors: () -> Void
let openFile: () -> Void
let toggleDark: () -> Void
let convertToPresetTheme: () -> Void
init(context: AccountContext, updateState: @escaping ((EditThemeControllerState) -> EditThemeControllerState) -> Void, openColors: @escaping () -> Void, openFile: @escaping () -> Void, toggleDark: @escaping () -> Void, convertToPresetTheme: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.openColors = openColors
self.openFile = openFile
self.toggleDark = toggleDark
self.convertToPresetTheme = convertToPresetTheme
}
}
private enum EditThemeEntryTag: ItemListItemTag {
case title
case slug
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? EditThemeEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum EditThemeControllerSection: Int32 {
case info
case chatPreview
}
private enum EditThemeControllerEntry: ItemListNodeEntry {
case title(PresentationTheme, PresentationStrings, String, String, Bool)
case slug(PresentationTheme, PresentationStrings, String, String, Bool)
case slugInfo(PresentationTheme, String)
case chatPreviewHeader(PresentationTheme, String)
case chatPreview(PresentationTheme, PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, [ChatPreviewMessageItem])
case changeColors(PresentationTheme, String)
case toggleDark(PresentationTheme, String)
case convertToPresetTheme(PresentationTheme, String)
case uploadTheme(PresentationTheme, String)
case uploadInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .title, .slug, .slugInfo:
return EditThemeControllerSection.info.rawValue
case .chatPreviewHeader, .chatPreview, .changeColors, .toggleDark, .convertToPresetTheme, .uploadTheme, .uploadInfo:
return EditThemeControllerSection.chatPreview.rawValue
}
}
var stableId: Int32 {
switch self {
case .title:
return 0
case .slug:
return 1
case .slugInfo:
return 2
case .chatPreviewHeader:
return 3
case .chatPreview:
return 4
case .changeColors:
return 5
case .toggleDark:
return 6
case .convertToPresetTheme:
return 7
case .uploadTheme:
return 8
case .uploadInfo:
return 9
}
}
static func ==(lhs: EditThemeControllerEntry, rhs: EditThemeControllerEntry) -> Bool {
switch lhs {
case let .title(lhsTheme, lhsStrings, lhsTitle, lhsValue, lhsDone):
if case let .title(rhsTheme, rhsStrings, rhsTitle, rhsValue, rhsDone) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsDone == rhsDone {
return true
} else {
return false
}
case let .slug(lhsTheme, lhsStrings, lhsTitle, lhsValue, lhsEnabled):
if case let .slug(rhsTheme, rhsStrings, rhsTitle, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
case let .slugInfo(lhsTheme, lhsText):
if case let .slugInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chatPreviewHeader(lhsTheme, lhsText):
if case let .chatPreviewHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chatPreview(lhsTheme, lhsComponentTheme, lhsWallpaper, lhsFontSize, lhsChatBubbleCorners, lhsStrings, lhsTimeFormat, lhsNameOrder, lhsItems):
if case let .chatPreview(rhsTheme, rhsComponentTheme, rhsWallpaper, rhsFontSize, rhsChatBubbleCorners, rhsStrings, rhsTimeFormat, rhsNameOrder, rhsItems) = rhs, lhsComponentTheme === rhsComponentTheme, lhsTheme === rhsTheme, lhsWallpaper == rhsWallpaper, lhsFontSize == rhsFontSize, lhsChatBubbleCorners == rhsChatBubbleCorners, lhsStrings === rhsStrings, lhsTimeFormat == rhsTimeFormat, lhsNameOrder == rhsNameOrder, lhsItems == rhsItems {
return true
} else {
return false
}
case let .changeColors(lhsTheme, lhsText):
if case let .changeColors(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .toggleDark(lhsTheme, lhsText):
if case let .toggleDark(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .convertToPresetTheme(lhsTheme, lhsText):
if case let .convertToPresetTheme(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .uploadTheme(lhsTheme, lhsText):
if case let .uploadTheme(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .uploadInfo(lhsTheme, lhsText):
if case let .uploadInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: EditThemeControllerEntry, rhs: EditThemeControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! EditThemeControllerArguments
switch self {
case let .title(_, _, title, text, _):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(), text: text, placeholder: title, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .default, clearType: .onFocus, tag: EditThemeEntryTag.title, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.title = value
return state
}
}, action: {
})
case let .slug(_, _, title, text, enabled):
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "t.me/addtheme/", textColor: presentationData.theme.list.itemPrimaryTextColor), text: text, placeholder: title, type: .username, clearType: .onFocus, enabled: enabled, tag: EditThemeEntryTag.slug, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.slug = value
return state
}
}, action: {
})
case let .slugInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .chatPreviewHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .chatPreview(theme, componentTheme, wallpaper, fontSize, chatBubbleCorners, strings, dateTimeFormat, nameDisplayOrder, items):
return ThemeSettingsChatPreviewItem(context: arguments.context, systemStyle: .glass, theme: theme, componentTheme: componentTheme, strings: strings, sectionId: self.section, fontSize: fontSize, chatBubbleCorners: chatBubbleCorners, wallpaper: wallpaper, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, messageItems: items)
case let .changeColors(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openColors()
})
case let .toggleDark(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.toggleDark()
})
case let .convertToPresetTheme(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.convertToPresetTheme()
})
case let .uploadTheme(_, text):
return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openFile()
})
case let .uploadInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
}
}
}
public enum EditThemeControllerMode: Equatable {
case create(PresentationTheme?, TelegramThemeSettings?)
case edit(PresentationCloudTheme)
}
private struct EditThemeControllerState: Equatable {
var mode: EditThemeControllerMode
var title: String
var slug: String
var updatedTheme: PresentationTheme?
var updating: Bool
var converting: Bool
}
private func editThemeControllerEntries(presentationData: PresentationData, state: EditThemeControllerState, previewTheme: PresentationTheme, hasSettings: Bool) -> [EditThemeControllerEntry] {
var entries: [EditThemeControllerEntry] = []
var isCreate = false
if case .create = state.mode {
isCreate = true
}
entries.append(.title(presentationData.theme, presentationData.strings, presentationData.strings.EditTheme_Title, state.title, isCreate))
let infoText: String
let uploadText: String
let uploadInfo: String
let previewIncomingReplyName: String
let previewIncomingReplyText: String
let previewIncomingText: String
let previewOutgoingText: String
switch state.mode {
case .create:
infoText = presentationData.strings.EditTheme_Create_TopInfo
uploadText = presentationData.strings.EditTheme_UploadNewTheme
uploadInfo = presentationData.strings.EditTheme_Create_BottomInfo
previewIncomingReplyName = presentationData.strings.EditTheme_Create_Preview_IncomingReplyName
previewIncomingReplyText = presentationData.strings.EditTheme_Create_Preview_IncomingReplyText
previewIncomingText = presentationData.strings.EditTheme_Create_Preview_IncomingText
previewOutgoingText = presentationData.strings.EditTheme_Create_Preview_OutgoingText
case let .edit(theme):
if let _ = theme.theme.file {
infoText = presentationData.strings.EditTheme_Edit_TopInfo
uploadText = presentationData.strings.EditTheme_UploadEditedTheme
uploadInfo = presentationData.strings.EditTheme_Edit_BottomInfo
previewIncomingReplyName = presentationData.strings.EditTheme_Expand_Preview_IncomingReplyName
previewIncomingReplyText = presentationData.strings.EditTheme_Expand_Preview_IncomingReplyText
previewIncomingText = presentationData.strings.EditTheme_Expand_Preview_IncomingText
previewOutgoingText = presentationData.strings.EditTheme_Expand_Preview_OutgoingText
} else {
infoText = presentationData.strings.EditTheme_Expand_TopInfo
uploadText = presentationData.strings.EditTheme_UploadNewTheme
uploadInfo = presentationData.strings.EditTheme_Expand_BottomInfo
previewIncomingReplyName = presentationData.strings.EditTheme_Edit_Preview_IncomingReplyName
previewIncomingReplyText = presentationData.strings.EditTheme_Edit_Preview_IncomingReplyText
previewIncomingText = presentationData.strings.EditTheme_Edit_Preview_IncomingText
previewOutgoingText = presentationData.strings.EditTheme_Edit_Preview_OutgoingText
}
}
if case .edit = state.mode {
entries.append(.slug(presentationData.theme, presentationData.strings, presentationData.strings.EditTheme_ShortLink, state.slug, true))
}
entries.append(.slugInfo(presentationData.theme, infoText))
entries.append(.chatPreviewHeader(presentationData.theme, presentationData.strings.EditTheme_Preview.uppercased()))
entries.append(.chatPreview(presentationData.theme, previewTheme, previewTheme.chat.defaultWallpaper, presentationData.chatFontSize, presentationData.chatBubbleCorners, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, [ChatPreviewMessageItem(outgoing: false, reply: (previewIncomingReplyName, previewIncomingReplyText), text: previewIncomingText, nameColor: .preset(.blue), backgroundEmojiId: nil), ChatPreviewMessageItem(outgoing: true, reply: nil, text: previewOutgoingText, nameColor: .preset(.blue), backgroundEmojiId: nil)]))
entries.append(.changeColors(presentationData.theme, presentationData.strings.EditTheme_ChangeColors))
if hasSettings {
if previewTheme.overallDarkAppearance {
// entries.append(.toggleDark(presentationData.theme, "Toggle Base Theme"))
}
} else {
if !isCreate {
// entries.append(.convertToPresetTheme(presentationData.theme, "Convert to Preset Theme"))
}
entries.append(.uploadTheme(presentationData.theme, uploadText))
entries.append(.uploadInfo(presentationData.theme, uploadInfo))
}
return entries
}
public func editThemeController(context: AccountContext, mode: EditThemeControllerMode, navigateToChat: ((PeerId) -> Void)? = nil, completion: ((PresentationThemeReference) -> Void)? = nil) -> ViewController {
let initialState: EditThemeControllerState
let previewThemePromise = Promise<PresentationTheme>()
let settingsPromise = Promise<TelegramThemeSettings?>(nil)
let hasSettings: Bool
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch mode {
case let .create(existingTheme, settings):
let theme: PresentationTheme
let wallpaper: TelegramWallpaper
if let existingTheme = existingTheme {
theme = existingTheme
wallpaper = theme.chat.defaultWallpaper
settingsPromise.set(.single(settings))
hasSettings = settings != nil
} else if let settings = settings {
theme = makePresentationTheme(settings: settings) ?? presentationData.theme
wallpaper = theme.chat.defaultWallpaper
settingsPromise.set(.single(settings))
hasSettings = true
} else {
theme = presentationData.theme
wallpaper = presentationData.chatWallpaper
settingsPromise.set(.single(nil))
hasSettings = false
}
initialState = EditThemeControllerState(mode: mode, title: generateThemeName(accentColor: theme.rootController.navigationBar.buttonColor), slug: "", updatedTheme: nil, updating: false, converting: false)
previewThemePromise.set(.single(theme.withUpdated(name: "", defaultWallpaper: wallpaper)))
case let .edit(info):
hasSettings = info.theme.settings != nil
settingsPromise.set(.single(info.theme.settings?.first))
if let file = info.theme.file, let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let theme = makePresentationTheme(data: data, resolvedWallpaper: info.resolvedWallpaper) {
if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 {
previewThemePromise.set(cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings)
|> map ({ wallpaper -> PresentationTheme in
if let wallpaper = wallpaper {
return theme.withUpdated(name: nil, defaultWallpaper: wallpaper.wallpaper)
} else {
return theme.withUpdated(name: nil, defaultWallpaper: .color(theme.chatList.backgroundColor.argb))
}
}))
} else {
previewThemePromise.set(.single(theme.withUpdated(name: nil, defaultWallpaper: info.resolvedWallpaper)))
}
} else {
previewThemePromise.set(.single(presentationData.theme.withUpdated(name: "", defaultWallpaper: presentationData.chatWallpaper)))
}
initialState = EditThemeControllerState(mode: mode, title: info.theme.title, slug: info.theme.slug, updatedTheme: nil, updating: false, converting: false)
}
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((EditThemeControllerState) -> EditThemeControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var errorImpl: ((EditThemeEntryTag) -> Void)?
var generalThemeReference: PresentationThemeReference?
if case let .edit(cloudTheme) = mode {
generalThemeReference = PresentationThemeReference.cloud(cloudTheme).generalThemeReference
} else if case let .create(existingTheme, _) = mode, existingTheme == nil {
generalThemeReference = PresentationThemeReference.builtin(presentationData.theme.referenceTheme)
}
let arguments = EditThemeControllerArguments(context: context, updateState: { f in
updateState(f)
}, openColors: {
let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get())
|> take(1)).start(next: { theme, previousSettings in
var controllerDismissImpl: (() -> Void)?
var generalThemeReference = generalThemeReference
if let settings = previousSettings {
generalThemeReference = .builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme))
}
let controller = ThemeAccentColorController(context: context, mode: .edit(settings: previousSettings, theme: theme, wallpaper: nil, generalThemeReference: generalThemeReference, defaultThemeReference: nil, create: false, completion: { updatedTheme, settings in
updateState { current in
var state = current
previewThemePromise.set(.single(updatedTheme))
state.updatedTheme = updatedTheme
return state
}
if previousSettings != nil {
settingsPromise.set(.single(settings))
}
controllerDismissImpl?()
}))
controllerDismissImpl = { [weak controller] in
controller?.dismiss()
}
pushControllerImpl?(controller)
})
}, openFile: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = legacyICloudFilePicker(theme: presentationData.theme, mode: .import, documentTypes: ["org.telegram.Telegram-iOS.theme"], completion: { urls in
if let url = urls.first{
if let data = try? Data(contentsOf: url), let theme = makePresentationTheme(data: data) {
if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 {
let _ = (cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings)
|> mapToSignal { wallpaper -> Signal<TelegramWallpaper?, NoError> in
if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper {
var convertedRepresentations: [ImageRepresentationWithReference] = []
convertedRepresentations.append(ImageRepresentationWithReference(representation: TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 100, height: 100), resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
return wallpaperDatas(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, alwaysShowThumbnailFirst: false, thumbnail: false, onlyFullSize: true, autoFetchFullSize: true, synchronousLoad: false)
|> mapToSignal { _, fullSizeData, complete -> Signal<TelegramWallpaper?, NoError> in
guard complete, let fullSizeData = fullSizeData else {
return .complete()
}
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true)
return .single(wallpaper.wallpaper)
}
} else {
return .single(nil)
}
}
|> deliverOnMainQueue).start(next: { wallpaper in
let updatedTheme = theme.withUpdated(name: nil, defaultWallpaper: wallpaper)
updateState { current in
var state = current
previewThemePromise.set(.single(updatedTheme))
state.updatedTheme = updatedTheme
return state
}
})
} else {
updateState { current in
var state = current
previewThemePromise.set(.single(theme))
state.updatedTheme = theme
return state
}
}
settingsPromise.set(.single(nil))
}
else {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.EditTheme_FileReadError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
}
})
presentControllerImpl?(controller, nil)
}, toggleDark: {
let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get())
|> take(1)).start(next: { theme, previousSettings in
var updatedTheme = theme
var updatedSettings: TelegramThemeSettings?
if let themeSettings = previousSettings {
if updatedTheme.referenceTheme == .night {
updatedTheme = updatedTheme.withUpdated(referenceTheme: .nightAccent)
updatedSettings = TelegramThemeSettings(baseTheme: .tinted, accentColor: themeSettings.accentColor, outgoingAccentColor: themeSettings.outgoingAccentColor, messageColors: themeSettings.messageColors, animateMessageColors: themeSettings.animateMessageColors, wallpaper: themeSettings.wallpaper)
if let settings = updatedSettings, let theme = makePresentationTheme(settings: settings) {
updatedTheme = theme
}
} else if updatedTheme.referenceTheme == .nightAccent {
updatedSettings = TelegramThemeSettings(baseTheme: .night, accentColor: themeSettings.accentColor, outgoingAccentColor: themeSettings.outgoingAccentColor, messageColors: themeSettings.messageColors, animateMessageColors: themeSettings.animateMessageColors, wallpaper: themeSettings.wallpaper)
if let settings = updatedSettings, let theme = makePresentationTheme(settings: settings) {
updatedTheme = theme
}
}
}
updateState { current in
var state = current
previewThemePromise.set(.single(updatedTheme))
state.updatedTheme = updatedTheme
return state
}
if previousSettings != nil {
settingsPromise.set(.single(updatedSettings))
}
})
}, convertToPresetTheme: {
let _ = (previewThemePromise.get()
|> take(1)).start(next: { theme in
var outgoingAccentColor: UIColor?
if case .classic = theme.referenceTheme.baseTheme {
outgoingAccentColor = theme.chat.message.outgoing.accentTextColor
}
let settings = TelegramThemeSettings(baseTheme: theme.referenceTheme.baseTheme, accentColor: theme.rootController.navigationBar.accentTextColor, outgoingAccentColor: outgoingAccentColor, messageColors: theme.chat.message.outgoing.bubble.withWallpaper.fill.map { $0.argb }, animateMessageColors: theme.chat.animateMessageColors, wallpaper: theme.chat.defaultWallpaper)
settingsPromise.set(.single(settings))
updateState { current in
var state = current
state.converting = true
return state
}
})
})
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), previewThemePromise.get(), settingsPromise.get())
|> map { presentationData, state, previewTheme, currentSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
})
var focusItemTag: ItemListItemTag?
if case .create = state.mode {
focusItemTag = EditThemeEntryTag.title
}
let rightNavigationButton: ItemListNavigationButton
if state.updating {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
let isComplete: Bool
if case .create = mode {
isComplete = !state.title.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty
} else {
isComplete = !state.title.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty && !state.slug.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty
}
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: isComplete, action: {
if state.title.count > 128 {
errorImpl?(.title)
return
}
if case .create = mode {
} else if state.slug.count < 5 || state.slug.count > 64 {
errorImpl?(.slug)
return
}
dismissInputImpl?()
arguments.updateState { current in
var state = current
state.updating = true
return state
}
let _ = (combineLatest(queue: Queue.mainQueue(), previewThemePromise.get(), settingsPromise.get())
|> take(1)).start(next: { previewTheme, settings in
let saveThemeTemplateFile: (String, LocalFileMediaResource, @escaping () -> Void) -> Void = { title, resource, completion in
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: resource.fileId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgtheme-ios", size: nil, attributes: [.FileName(fileName: "\(title).tgios-theme")], alternativeRepresentations: [])
let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
let _ = enqueueMessages(account: context.account, peerId: context.account.peerId, messages: [message]).start()
if let navigateToChat = navigateToChat {
presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.EditTheme_ThemeTemplateAlertTitle, text: presentationData.strings.EditTheme_ThemeTemplateAlertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_SavedMessages, action: {
completion()
navigateToChat(context.account.peerId)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
completion()
})], actionLayout: .vertical), nil)
} else {
completion()
}
}
let theme: PresentationTheme?
let hasCustomFile: Bool
if let updatedTheme = state.updatedTheme {
theme = updatedTheme.withUpdated(name: state.title, defaultWallpaper: nil)
hasCustomFile = true
} else {
if case let .edit(info) = mode, let _ = info.theme.file {
theme = nil
hasCustomFile = true
} else {
theme = previewTheme.withUpdated(name: state.title, defaultWallpaper: nil)
hasCustomFile = false
}
}
let themeResource: LocalFileMediaResource?
let themeData: Data?
let themeThumbnailData: Data?
if let theme = theme, let themeString = encodePresentationTheme(theme), let data = themeString.data(using: .utf8) {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
themeResource = resource
themeData = data
var wallpaperImage: UIImage?
if case .file = theme.chat.defaultWallpaper {
wallpaperImage = chatControllerBackgroundImage(theme: theme, wallpaper: theme.chat.defaultWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: false)
}
let themeThumbnail = generateImage(CGSize(width: 213, height: 320.0), contextGenerator: { size, context in
if let image = generateImage(CGSize(width: 194.0, height: 291.0), contextGenerator: { size, c in
drawThemeImage(context: c, theme: theme, wallpaperImage: wallpaperImage, size: size)
})?.cgImage {
context.draw(image, in: CGRect(origin: CGPoint(), size: size))
}
}, scale: 1.0)
themeThumbnailData = themeThumbnail?.jpegData(compressionQuality: 0.6)
} else {
themeResource = nil
themeData = nil
themeThumbnailData = nil
}
let resolvedWallpaper: TelegramWallpaper?
if let theme = theme, case let .file(file) = theme.chat.defaultWallpaper, file.id != 0 {
resolvedWallpaper = theme.chat.defaultWallpaper
updateCachedWallpaper(engine: context.engine, wallpaper: theme.chat.defaultWallpaper)
} else {
resolvedWallpaper = nil
}
let prepare: Signal<CreateThemeResult, CreateThemeError>
if let resolvedWallpaper = resolvedWallpaper, case let .file(file) = resolvedWallpaper, resolvedWallpaper.isPattern {
let resource = file.file.resource
var data: Data?
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
data = maybeData
} else if let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
data = maybeData
}
if let data = data {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
prepare = .complete()
} else {
prepare = .complete()
}
} else {
prepare = .complete()
}
switch mode {
case .create:
if let themeResource = themeResource {
let _ = (prepare |> then(createTheme(account: context.account, title: state.title, resource: themeResource, thumbnailData: themeThumbnailData, settings: settings.flatMap { [$0] })
|> deliverOnMainQueue)).start(next: { next in
if case let .result(resultTheme) = next {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start()
let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
if let resource = resultTheme.file?.resource, let data = themeData {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
}
let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id))
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
themeSpecificChatWallpapers[themeReference.index] = nil
return PresentationThemeSettings(theme: themeReference, themePreferredBaseTheme: current.themePreferredBaseTheme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, reduceMotion: current.reduceMotion)
}) |> deliverOnMainQueue).start(completed: {
if !hasCustomFile {
saveThemeTemplateFile(state.title, themeResource, {
dismissImpl?()
})
} else {
dismissImpl?()
}
})
}
}, error: { error in
arguments.updateState { current in
var state = current
state.updating = false
return state
}
})
}
case let .edit(info):
let _ = (prepare |> then(updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme, title: state.title, slug: state.slug, resource: state.converting ? nil : themeResource, settings: settings.flatMap { [$0] })
|> deliverOnMainQueue)).start(next: { next in
if case let .result(resultTheme) = next {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start()
let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
if let resource = resultTheme.file?.resource, let data = themeData {
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
}
let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id))
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
themeSpecificChatWallpapers[themeReference.index] = nil
return PresentationThemeSettings(theme: themeReference, themePreferredBaseTheme: current.themePreferredBaseTheme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, useSystemFont: current.useSystemFont, fontSize: current.fontSize, listsFontSize: current.listsFontSize, chatBubbleSettings: current.chatBubbleSettings, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, reduceMotion: current.reduceMotion)
}) |> deliverOnMainQueue).start(completed: {
if let themeResource = themeResource, !hasCustomFile {
saveThemeTemplateFile(state.title, themeResource, {
dismissImpl?()
})
} else {
dismissImpl?()
}
})
}
}, error: { error in
arguments.updateState { current in
var state = current
state.updating = false
return state
}
var errorText: String?
switch error {
case .slugOccupied:
errorText = presentationData.strings.EditTheme_ErrorLinkTaken
case .slugInvalid:
errorText = presentationData.strings.EditTheme_ErrorInvalidCharacters
default:
break
}
if let errorText = errorText {
presentControllerImpl?(textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}
})
}
})
})
}
let title: String
switch mode {
case .create:
title = presentationData.strings.EditTheme_CreateTitle
case let .edit(theme):
if theme.theme.file == nil {
title = presentationData.strings.EditTheme_CreateTitle
} else {
title = presentationData.strings.EditTheme_EditTitle
}
}
let hasSettings = hasSettings || currentSettings != nil
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: editThemeControllerEntries(presentationData: presentationData, state: state, previewTheme: previewTheme, hasSettings: hasSettings), style: .blocks, focusItemTag: focusItemTag, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
let _ = controller?.dismiss()
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
let hapticFeedback = HapticFeedback()
errorImpl = { [weak controller] targetTag in
hapticFeedback.error()
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListSingleLineInputItemNode, let tag = itemNode.tag, tag.isEqual(to: targetTag) {
itemNode.animateError()
}
}
}
return controller
}
@@ -0,0 +1,596 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import TelegramStringFormatting
import AccountContext
import DeviceLocationManager
import Geocoding
import WallpaperResources
import Sunrise
import ThemeSettingsThemeItem
private enum TriggerMode {
case system
case none
case timeBased
case brightness
}
private enum TimeBasedManualField {
case from
case to
}
private final class ThemeAutoNightSettingsControllerArguments {
let context: AccountContext
let updateMode: (TriggerMode) -> Void
let updateTimeBasedAutomatic: (Bool) -> Void
let openTimeBasedManual: (TimeBasedManualField) -> Void
let updateTimeBasedAutomaticLocation: () -> Void
let updateAutomaticBrightness: (Double) -> Void
let updateTheme: (PresentationThemeReference) -> Void
init(context: AccountContext, updateMode: @escaping (TriggerMode) -> Void, updateTimeBasedAutomatic: @escaping (Bool) -> Void, openTimeBasedManual: @escaping (TimeBasedManualField) -> Void, updateTimeBasedAutomaticLocation: @escaping () -> Void, updateAutomaticBrightness: @escaping (Double) -> Void, updateTheme: @escaping (PresentationThemeReference) -> Void) {
self.context = context
self.updateMode = updateMode
self.updateTimeBasedAutomatic = updateTimeBasedAutomatic
self.openTimeBasedManual = openTimeBasedManual
self.updateTimeBasedAutomaticLocation = updateTimeBasedAutomaticLocation
self.updateAutomaticBrightness = updateAutomaticBrightness
self.updateTheme = updateTheme
}
}
private enum ThemeAutoNightSettingsControllerSection: Int32 {
case mode
case settings
case theme
}
private enum ThemeAutoNightSettingsControllerEntry: ItemListNodeEntry {
case modeSystem(PresentationTheme, String, Bool)
case modeDisabled(PresentationTheme, String, Bool)
case modeTimeBased(PresentationTheme, String, Bool)
case modeBrightness(PresentationTheme, String, Bool)
case settingsHeader(PresentationTheme, String)
case timeBasedAutomaticLocation(PresentationTheme, String, Bool)
case timeBasedAutomaticLocationValue(PresentationTheme, String, String)
case timeBasedManualFrom(PresentationTheme, String, String)
case timeBasedManualTo(PresentationTheme, String, String)
case brightnessValue(PresentationTheme, Double)
case settingInfo(PresentationTheme, String)
case themeHeader(PresentationTheme, String)
case themeItem(PresentationTheme, PresentationStrings, [PresentationThemeReference], [PresentationThemeReference], PresentationThemeReference, [Int64: PresentationThemeAccentColor], [Int64: TelegramWallpaper])
var section: ItemListSectionId {
switch self {
case .modeSystem, .modeDisabled, .modeTimeBased, .modeBrightness:
return ThemeAutoNightSettingsControllerSection.mode.rawValue
case .settingsHeader, .timeBasedAutomaticLocation, .timeBasedAutomaticLocationValue, .timeBasedManualFrom, .timeBasedManualTo, .brightnessValue, .settingInfo:
return ThemeAutoNightSettingsControllerSection.settings.rawValue
case .themeHeader, .themeItem:
return ThemeAutoNightSettingsControllerSection.theme.rawValue
}
}
var stableId: Int32 {
switch self {
case .modeSystem:
return 0
case .modeDisabled:
return 1
case .modeTimeBased:
return 2
case .modeBrightness:
return 3
case .settingsHeader:
return 4
case .timeBasedAutomaticLocation:
return 5
case .timeBasedAutomaticLocationValue:
return 6
case .timeBasedManualFrom:
return 7
case .timeBasedManualTo:
return 8
case .brightnessValue:
return 9
case .settingInfo:
return 10
case .themeHeader:
return 11
case .themeItem:
return 12
}
}
static func ==(lhs: ThemeAutoNightSettingsControllerEntry, rhs: ThemeAutoNightSettingsControllerEntry) -> Bool {
switch lhs {
case let .modeSystem(lhsTheme, lhsTitle, lhsValue):
if case let .modeSystem(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .modeDisabled(lhsTheme, lhsTitle, lhsValue):
if case let .modeDisabled(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .modeTimeBased(lhsTheme, lhsTitle, lhsValue):
if case let .modeTimeBased(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .modeBrightness(lhsTheme, lhsTitle, lhsValue):
if case let .modeBrightness(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .settingsHeader(lhsTheme, lhsTitle):
if case let .settingsHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .timeBasedAutomaticLocation(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedAutomaticLocation(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .timeBasedAutomaticLocationValue(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedAutomaticLocationValue(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .timeBasedManualFrom(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedManualFrom(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .timeBasedManualTo(lhsTheme, lhsTitle, lhsValue):
if case let .timeBasedManualTo(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
} else {
return false
}
case let .brightnessValue(lhsTheme, lhsValue):
if case let .brightnessValue(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .settingInfo(lhsTheme, lhsValue):
if case let .settingInfo(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .themeHeader(lhsTheme, lhsValue):
if case let .themeHeader(rhsTheme, rhsValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue {
return true
} else {
return false
}
case let .themeItem(lhsTheme, lhsStrings, lhsThemes, lhsAllThemes, lhsCurrentTheme, lhsThemeAccentColors, lhsThemeChatWallpapers):
if case let .themeItem(rhsTheme, rhsStrings, rhsThemes, rhsAllThemes, rhsCurrentTheme, rhsThemeAccentColors, rhsThemeChatWallpapers) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsThemes == rhsThemes, lhsAllThemes == rhsAllThemes, lhsCurrentTheme == rhsCurrentTheme, lhsThemeAccentColors == rhsThemeAccentColors, lhsThemeChatWallpapers == rhsThemeChatWallpapers {
return true
} else {
return false
}
}
}
static func <(lhs: ThemeAutoNightSettingsControllerEntry, rhs: ThemeAutoNightSettingsControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! ThemeAutoNightSettingsControllerArguments
switch self {
case let .modeSystem(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.system)
})
case let .modeDisabled(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.none)
})
case let .modeTimeBased(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.timeBased)
})
case let .modeBrightness(_, title, value):
return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: title, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateMode(.brightness)
})
case let .settingsHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .timeBasedAutomaticLocation(_, title, value):
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateTimeBasedAutomatic(value)
})
case let .timeBasedAutomaticLocationValue(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: title, titleColor: .accent, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.updateTimeBasedAutomaticLocation()
})
case let .timeBasedManualFrom(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openTimeBasedManual(.from)
})
case let .timeBasedManualTo(_, title, value):
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: title, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openTimeBasedManual(.to)
})
case let .brightnessValue(theme, value):
return ThemeSettingsBrightnessItem(theme: theme, value: Int32(value * 100.0), sectionId: self.section, updated: { value in
arguments.updateAutomaticBrightness(Double(value) / 100.0)
})
case let .settingInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .themeHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .themeItem(theme, strings, themes, allThemes, currentTheme, themeSpecificAccentColors, themeSpecificChatWallpapers):
return ThemeSettingsThemeItem(context: arguments.context, theme: theme, strings: strings, sectionId: self.section, themes: themes, allThemes: allThemes, displayUnsupported: false, themeSpecificAccentColors: themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, themePreferredBaseTheme: [:], currentTheme: currentTheme, updatedTheme: { theme in
arguments.updateTheme(theme)
}, contextAction: nil)
}
}
}
private func themeAutoNightSettingsControllerEntries(theme: PresentationTheme, strings: PresentationStrings, settings: PresentationThemeSettings, switchSetting: AutomaticThemeSwitchSetting, availableThemes: [PresentationThemeReference], dateTimeFormat: PresentationDateTimeFormat) -> [ThemeAutoNightSettingsControllerEntry] {
var entries: [ThemeAutoNightSettingsControllerEntry] = []
let activeTriggerMode: TriggerMode
switch switchSetting.trigger {
case .system:
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
activeTriggerMode = .system
} else {
activeTriggerMode = .none
}
case .explicitNone:
activeTriggerMode = .none
case .timeBased:
activeTriggerMode = .timeBased
case .brightness:
activeTriggerMode = .brightness
}
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
entries.append(.modeSystem(theme, strings.AutoNightTheme_System, activeTriggerMode == .system))
}
entries.append(.modeDisabled(theme, strings.AutoNightTheme_Disabled, activeTriggerMode == .none))
entries.append(.modeTimeBased(theme, strings.AutoNightTheme_Scheduled, activeTriggerMode == .timeBased))
entries.append(.modeBrightness(theme, strings.AutoNightTheme_Automatic, activeTriggerMode == .brightness))
switch switchSetting.trigger {
case .system, .explicitNone:
break
case let .timeBased(setting):
entries.append(.settingsHeader(theme, strings.AutoNightTheme_ScheduleSection))
var automaticLocation = false
if case .automatic = setting {
automaticLocation = true
}
entries.append(.timeBasedAutomaticLocation(theme, strings.AutoNightTheme_UseSunsetSunrise, automaticLocation))
switch setting {
case let .automatic(latitude, longitude, localizedName):
let calculator = EDSunriseSet(date: Date(), timezone: TimeZone.current, latitude: latitude, longitude: longitude)!
let sunset = roundTimeToDay(Int32(calculator.sunset.timeIntervalSince1970))
let sunrise = roundTimeToDay(Int32(calculator.sunrise.timeIntervalSince1970))
entries.append(.timeBasedAutomaticLocationValue(theme, strings.AutoNightTheme_UpdateLocation, localizedName))
if sunset != 0 || sunrise != 0 {
entries.append(.settingInfo(theme, strings.AutoNightTheme_LocationHelp(stringForMessageTimestamp(timestamp: sunset, dateTimeFormat: dateTimeFormat, local: false), stringForMessageTimestamp(timestamp: sunrise, dateTimeFormat: dateTimeFormat, local: false)).string))
}
case let .manual(fromSeconds, toSeconds):
entries.append(.timeBasedManualFrom(theme, strings.AutoNightTheme_ScheduledFrom, stringForMessageTimestamp(timestamp: fromSeconds, dateTimeFormat: dateTimeFormat, local: false)))
entries.append(.timeBasedManualTo(theme, strings.AutoNightTheme_ScheduledTo, stringForMessageTimestamp(timestamp: toSeconds, dateTimeFormat: dateTimeFormat, local: false)))
}
case let .brightness(threshold):
entries.append(.settingsHeader(theme, strings.AutoNightTheme_AutomaticSection))
entries.append(.brightnessValue(theme, threshold))
entries.append(.settingInfo(theme, strings.AutoNightTheme_AutomaticHelp("\(Int(threshold * 100.0))").string.replacingOccurrences(of: "%%", with: "%")))
}
switch switchSetting.trigger {
case .explicitNone:
break
case .system, .timeBased, .brightness:
entries.append(.themeHeader(theme, strings.AutoNightTheme_PreferredTheme))
let generalThemes: [PresentationThemeReference] = availableThemes.filter { reference in
if case let .cloud(theme) = reference {
return theme.theme.settings == nil
} else {
return true
}
}
entries.append(.themeItem(theme, strings, generalThemes, availableThemes, switchSetting.theme, settings.themeSpecificAccentColors, settings.themeSpecificChatWallpapers))
}
return entries
}
private func roundTimeToDay(_ timestamp: Int32) -> Int32 {
let calendar = Calendar.current
let offset = 0
let components = calendar.dateComponents([.hour, .minute, .second], from: Date(timeIntervalSince1970: Double(timestamp + Int32(offset))))
return Int32(components.hour! * 60 * 60 + components.minute! * 60 + components.second!)
}
private func areSettingsValid(_ settings: AutomaticThemeSwitchSetting) -> Bool {
switch settings.trigger {
case .system, .explicitNone, .brightness:
return true
case let .timeBased(setting):
switch setting {
case let .automatic(latitude, longitude, _):
if !latitude.isZero || !longitude.isZero {
return true
} else {
return false
}
case .manual:
return true
}
}
}
public func themeAutoNightSettingsController(context: AccountContext) -> ViewController {
var presentControllerImpl: ((ViewController) -> Void)?
let actionsDisposable = DisposableSet()
let updateAutomaticBrightnessDisposable = MetaDisposable()
let stagingSettingsPromise = ValuePromise<AutomaticThemeSwitchSetting?>(nil)
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings])
let updateLocationDisposable = MetaDisposable()
actionsDisposable.add(updateLocationDisposable)
let updateSettings: (@escaping (AutomaticThemeSwitchSetting) -> AutomaticThemeSwitchSetting) -> Void = { f in
let _ = (combineLatest(stagingSettingsPromise.get(), sharedData)
|> take(1)
|> deliverOnMainQueue).start(next: { stagingSettings, sharedData in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let updated = f(stagingSettings ?? settings.automaticThemeSwitchSetting)
stagingSettingsPromise.set(updated)
if areSettingsValid(updated) {
let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
var current = current
current.automaticThemeSwitchSetting = updated
return current
}).start()
}
})
}
let forceUpdateLocation: () -> Void = {
let locationCoordinates = Signal<(Double, Double), NoError> { subscriber in
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in
subscriber.putNext((location.coordinate.latitude, location.coordinate.longitude))
subscriber.putCompletion()
})
}
let geocodedLocation = locationCoordinates
|> mapToSignal { coordinates -> Signal<(Double, Double, String), NoError> in
return reverseGeocodeLocation(latitude: coordinates.0, longitude: coordinates.1)
|> map { locality in
return (coordinates.0, coordinates.1, locality?.city ?? "")
}
}
let disposable = (geocodedLocation
|> take(1)
|> deliverOnMainQueue).start(next: { location in
updateSettings { settings in
var settings = settings
if case let .timeBased(setting) = settings.trigger, case .automatic = setting {
settings.trigger = .timeBased(setting: .automatic(latitude: location.0, longitude: location.1, localizedName: location.2))
}
return settings
}
})
updateLocationDisposable.set(disposable)
}
let arguments = ThemeAutoNightSettingsControllerArguments(context: context, updateMode: { mode in
var updateLocation = false
updateSettings { settings in
var settings = settings
switch mode {
case .system:
settings.trigger = .system
case .none:
settings.trigger = .explicitNone
case .timeBased:
if case .timeBased = settings.trigger {
} else {
settings.trigger = .timeBased(setting: .automatic(latitude: 0.0, longitude: 0.0, localizedName: ""))
updateLocation = true
}
case .brightness:
if case .brightness = settings.trigger {
} else {
settings.trigger = .brightness(threshold: 0.2)
}
}
if updateLocation {
forceUpdateLocation()
}
return settings
}
}, updateTimeBasedAutomatic: { value in
var updateLocation = false
updateSettings { settings in
var settings = settings
if case let .timeBased(setting) = settings.trigger {
switch setting {
case .automatic:
if !value {
settings.trigger = .timeBased(setting: .manual(fromSeconds: 22 * 60 * 60, toSeconds: 9 * 60 * 60))
}
case .manual:
if value {
settings.trigger = .timeBased(setting: .automatic(latitude: 0.0, longitude: 0.0, localizedName: ""))
updateLocation = true
}
}
}
if updateLocation {
forceUpdateLocation()
}
return settings
}
}, openTimeBasedManual: { field in
var currentValue: Int32
switch field {
case .from:
currentValue = 22 * 60 * 60
case .to:
currentValue = 9 * 60 * 60
}
updateSettings { settings in
let settings = settings
switch settings.trigger {
case let .timeBased(setting):
switch setting {
case let .manual(fromSeconds, toSeconds):
switch field {
case .from:
currentValue = fromSeconds
case .to:
currentValue = toSeconds
}
default:
break
}
default:
break
}
presentControllerImpl?(ThemeAutoNightTimeSelectionActionSheet(context: context, currentValue: currentValue, applyValue: { value in
guard let value = value else {
return
}
updateSettings { settings in
var settings = settings
switch settings.trigger {
case let .timeBased(setting):
switch setting {
case var .manual(fromSeconds, toSeconds):
switch field {
case .from:
fromSeconds = value
case .to:
toSeconds = value
}
settings.trigger = .timeBased(setting: .manual(fromSeconds: fromSeconds, toSeconds: toSeconds))
default:
break
}
default:
break
}
return settings
}
}))
return settings
}
}, updateTimeBasedAutomaticLocation: {
forceUpdateLocation()
}, updateAutomaticBrightness: { value in
updateAutomaticBrightnessDisposable.set((Signal<Never, NoError>.complete()
|> delay(0.1, queue: Queue.mainQueue())).start(completed: {
updateSettings { settings in
var settings = settings
switch settings.trigger {
case .brightness:
settings.trigger = .brightness(threshold: max(0.0, min(1.0, value)))
default:
break
}
return settings
}
}))
}, updateTheme: { theme in
guard let presentationTheme = makePresentationTheme(mediaBox: context.sharedContext.accountManager.mediaBox, themeReference: theme) else {
return
}
let resolvedWallpaper: Signal<TelegramWallpaper?, NoError>
if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 {
resolvedWallpaper = cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings)
|> map { wallpaper -> TelegramWallpaper? in
return wallpaper?.wallpaper
}
} else {
resolvedWallpaper = .single(nil)
}
let _ = (resolvedWallpaper
|> mapToSignal { resolvedWallpaper -> Signal<Void, NoError> in
var updatedTheme = theme
if case let .cloud(info) = theme {
updatedTheme = .cloud(PresentationCloudTheme(theme: info.theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: info.theme.isCreator ? context.account.id : nil))
}
updateSettings { settings in
var settings = settings
settings.theme = updatedTheme
return settings
}
return .complete()
}).start()
})
let cloudThemes = Promise<[TelegramTheme]>()
let updatedCloudThemes = telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager)
cloudThemes.set(updatedCloudThemes)
let signal = combineLatest(context.sharedContext.presentationData |> deliverOnMainQueue, sharedData |> deliverOnMainQueue, cloudThemes.get() |> deliverOnMainQueue, stagingSettingsPromise.get() |> deliverOnMainQueue)
|> map { presentationData, sharedData, cloudThemes, stagingSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
let defaultThemes: [PresentationThemeReference] = [.builtin(.night), .builtin(.nightAccent)]
let cloudThemes: [PresentationThemeReference] = cloudThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? context.account.id : nil)) }
var availableThemes = defaultThemes
availableThemes.append(contentsOf: cloudThemes)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.AutoNightTheme_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeAutoNightSettingsControllerEntries(theme: presentationData.theme, strings: presentationData.strings, settings: settings, switchSetting: stagingSettings ?? settings.automaticThemeSwitchSetting, availableThemes: availableThemes, dateTimeFormat: presentationData.dateTimeFormat), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
controller.alwaysSynchronous = true
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
return controller
}
@@ -0,0 +1,132 @@
import Foundation
import Display
import AsyncDisplayKit
import UIKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import AccountContext
import UIKitRuntimeUtils
final class ThemeAutoNightTimeSelectionActionSheet: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, currentValue: Int32, emptyTitle: String? = nil, applyValue: @escaping (Int32?) -> Void) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
self._ready.set(.single(true))
var updatedValue = currentValue
var items: [ActionSheetItem] = []
items.append(ThemeAutoNightTimeSelectionActionSheetItem(strings: strings, currentValue: currentValue, valueChanged: { value in
updatedValue = value
}))
if let emptyTitle = emptyTitle {
items.append(ActionSheetButtonItem(title: emptyTitle, action: { [weak self] in
self?.dismissAnimated()
applyValue(nil)
}))
}
items.append(ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in
self?.dismissAnimated()
applyValue(updatedValue)
}))
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
}),
])
])
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class ThemeAutoNightTimeSelectionActionSheetItem: ActionSheetItem {
let strings: PresentationStrings
let currentValue: Int32
let valueChanged: (Int32) -> Void
init(strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.strings = strings
self.currentValue = currentValue
self.valueChanged = valueChanged
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ThemeAutoNightTimeSelectionActionSheetItemNode(theme: theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class ThemeAutoNightTimeSelectionActionSheetItemNode: ActionSheetItemNode {
private let theme: ActionSheetControllerTheme
private let strings: PresentationStrings
private let valueChanged: (Int32) -> Void
private let pickerView: UIDatePicker
init(theme: ActionSheetControllerTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.valueChanged = valueChanged
UILabel.setDateLabel(theme.primaryTextColor)
self.pickerView = UIDatePicker()
self.pickerView.datePickerMode = .countDownTimer
self.pickerView.datePickerMode = .time
self.pickerView.timeZone = TimeZone(secondsFromGMT: 0)
self.pickerView.date = Date(timeIntervalSince1970: Double(currentValue))
self.pickerView.locale = Locale.current
if #available(iOS 13.4, *) {
self.pickerView.preferredDatePickerStyle = .wheels
}
self.pickerView.setValue(theme.primaryTextColor, forKey: "textColor")
super.init(theme: theme)
self.view.addSubview(self.pickerView)
self.pickerView.addTarget(self, action: #selector(self.datePickerUpdated), for: .valueChanged)
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 216.0)
self.pickerView.frame = CGRect(origin: CGPoint(), size: size)
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
@objc private func datePickerUpdated() {
self.valueChanged(Int32(self.pickerView.date.timeIntervalSince1970))
}
}
@@ -0,0 +1,45 @@
import Foundation
import TelegramUIPreferences
import TelegramPresentationData
import TelegramCore
private func patternWallpaper(data: BuiltinWallpaperData, colors: [UInt32], intensity: Int32?, rotation: Int32?) -> TelegramWallpaper {
return defaultBuiltinWallpaper(data: data, colors: colors, intensity: intensity ?? 50, rotation: rotation)
}
var dayClassicColorPresets: [PresentationThemeAccentColor] = [
// Pink with Blue
PresentationThemeAccentColor(index: 106, baseColor: .preset, accentColor: 0xfff55783, bubbleColors: [0xffd6f5ff, 0xffc9fdfe], wallpaper: patternWallpaper(data: .default, colors: [0x8dc0eb, 0xb9d1ea, 0xc6b1ef, 0xebd7ef], intensity: 50, rotation: nil)),
// Pink with Gold
PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0xFFFF5FA9, bubbleColors: [0xFFFFF4D7], wallpaper: patternWallpaper(data: .variant12, colors: [0xeaa36e, 0xf0e486, 0xf29ebf, 0xe8c06e], intensity: 50, rotation: nil)),
// Green
PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xFF5A9E29, bubbleColors: [0xffFFF8DF], wallpaper: patternWallpaper(data: .variant13, colors: [0x7fc289, 0xe4d573, 0xafd677, 0xf0c07a], intensity: 50, rotation: nil)),
// Purple
PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0xFF7E5FE5, bubbleColors: [0xFFF5e2FF], wallpaper: patternWallpaper(data: .variant14, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: 50, rotation: nil)),
// Light Blue
PresentationThemeAccentColor(index: 107, baseColor: .preset, accentColor: 0xFF2CB9ED, bubbleColors: [0xFFADF7B5, 0xFFFCFF8B], wallpaper: patternWallpaper(data: .variant3, colors: [0x1a2e1a, 0x47623c, 0x222e24, 0x314429], intensity: 50, rotation: nil)),
// Mint
PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xFF199972, bubbleColors: [0xFFFFFEC7], wallpaper: patternWallpaper(data: .variant3, colors: [0xdceb92, 0x8fe1d6, 0x67a3f2, 0x85d685], intensity: 50, rotation: nil)),
// Pink with Green
PresentationThemeAccentColor(index: 105, baseColor: .preset, accentColor: 0xFFDA90D9, bubbleColors: [0xFF94FFF9, 0xFFCCFFC7], wallpaper: patternWallpaper(data: .variant9, colors: [0xffc3b2, 0xe2c0ff, 0xffe7b2], intensity: 50, rotation: nil))
]
var dayColorPresets: [PresentationThemeAccentColor] = [
PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0x0088ff, bubbleColors: [0x0088ff, 0xff53f4], wallpaper: nil),
PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0x00b09b, bubbleColors: [0xaee946, 0x00b09b], wallpaper: nil),
PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xd33213, bubbleColors: [0xf9db00, 0xd33213], wallpaper: nil),
PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xea8ced, bubbleColors: [0xea8ced, 0x00c2ed], wallpaper: nil)
]
var nightColorPresets: [PresentationThemeAccentColor] = [
// PresentationThemeAccentColor(index: 101, baseColor: .preset, accentColor: 0x0088ff, bubbleColors: [0x0088ff, 0xff53f4], wallpaper: patternWallpaper(data: .variant4, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: -35, rotation: nil)),
PresentationThemeAccentColor(index: 102, baseColor: .preset, accentColor: 0x00b09b, bubbleColors: [0xaee946, 0x00b09b], wallpaper: patternWallpaper(data: .variant9, colors: [0xe4b2ea, 0x8376c2, 0xeab9d9, 0xb493e6], intensity: -35, rotation: nil)),
PresentationThemeAccentColor(index: 103, baseColor: .preset, accentColor: 0xd33213, bubbleColors: [0xf9db00, 0xd33213], wallpaper: patternWallpaper(data: .variant2, colors: [0xfec496, 0xdd6cb9, 0x962fbf, 0x4f5bd5], intensity: -40, rotation: nil)),
PresentationThemeAccentColor(index: 104, baseColor: .preset, accentColor: 0xea8ced, bubbleColors: [0xea8ced, 0x00c2ed], wallpaper: patternWallpaper(data: .variant6, colors: [0x8adbf2, 0x888dec, 0xe39fea, 0x679ced], intensity: -30, rotation: nil))
]
@@ -0,0 +1,530 @@
import Foundation
import UIKit
import Display
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ShareController
import CounterControllerTitleView
import WallpaperResources
import OverlayStatusController
import AppBundle
import PresentationDataUtils
import UndoUI
import TelegramNotices
public enum ThemePreviewSource {
case settings(PresentationThemeReference, TelegramWallpaper?, Bool)
case theme(TelegramTheme)
case slug(String, TelegramMediaFile)
case themeSettings(String, TelegramThemeSettings)
case media(AnyMediaReference)
}
public final class ThemePreviewController: ViewController {
private let context: AccountContext
private let previewTheme: PresentationTheme
private let source: ThemePreviewSource
private let theme = Promise<TelegramTheme?>()
private let presentationTheme = Promise<PresentationTheme>()
private var controllerNode: ThemePreviewControllerNode {
return self.displayNode as! ThemePreviewControllerNode
}
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var validLayout: ContainerViewLayout?
private var didPlayPresentationAnimation = false
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var disposable: Disposable?
private var applyDisposable = MetaDisposable()
var customApply: (() -> Void)?
public init(context: AccountContext, previewTheme: PresentationTheme, source: ThemePreviewSource) {
self.context = context
self.previewTheme = previewTheme
self.source = source
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.presentationTheme.set(.single(previewTheme))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationTheme: self.previewTheme, presentationStrings: self.presentationData.strings))
self.blocksBackgroundWhenInOverlay = true
self.acceptsFocusWhenInOverlay = true
self.navigationPresentation = .modal
var hasInstallsCount = false
let themeName: String
switch source {
case let .theme(theme):
themeName = theme.title
self.theme.set(.single(theme)
|> then(
getTheme(account: context.account, slug: theme.slug)
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramTheme?, NoError> in
return .single(nil)
}
|> filter { $0 != nil }
))
hasInstallsCount = true
case let .slug(slug, _), let .themeSettings(slug, _):
self.theme.set(getTheme(account: context.account, slug: slug)
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramTheme?, NoError> in
return .single(nil)
})
themeName = previewTheme.name.string
self.presentationTheme.set(.single(self.previewTheme)
|> then(
self.theme.get()
|> mapToSignal { theme in
if let file = theme?.file {
return telegramThemeData(account: context.account, accountManager: context.sharedContext.accountManager, reference: .standalone(resource: file.resource))
|> mapToSignal { data -> Signal<PresentationTheme, NoError> in
guard let data = data, let presentationTheme = makePresentationTheme(data: data) else {
return .complete()
}
return .single(presentationTheme)
}
} else {
return .complete()
}
}
))
hasInstallsCount = true
case let .settings(themeReference, _, _):
if case let .cloud(theme) = themeReference {
self.theme.set(getTheme(account: context.account, slug: theme.theme.slug)
|> map(Optional.init)
|> `catch` { _ -> Signal<TelegramTheme?, NoError> in
return .single(nil)
})
if let emoticon = theme.theme.emoticon{
themeName = emoticon
} else {
themeName = theme.theme.title
hasInstallsCount = true
}
} else {
self.theme.set(.single(nil))
if [.builtin(.dayClassic), .builtin(.night)].contains(themeReference) {
themeName = "🏠"
} else {
themeName = previewTheme.name.string
}
}
default:
self.theme.set(.single(nil))
themeName = previewTheme.name.string
}
var isPreview = false
if case .settings = source {
isPreview = true
}
let titleView = CounterControllerTitleView(theme: self.previewTheme)
titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? " " : "")
self.navigationItem.titleView = titleView
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.statusBar.statusBarStyle = self.previewTheme.rootController.statusBarStyle.style
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
if !isPreview {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: self.previewTheme.rootController.navigationBar.accentTextColor), style: .plain, target: self, action: #selector(self.actionPressed))
}
self.disposable = (combineLatest(self.theme.get(), self.presentationTheme.get())
|> deliverOnMainQueue).start(next: { [weak self] theme, presentationTheme in
if let strongSelf = self, let theme = theme {
let titleView = CounterControllerTitleView(theme: strongSelf.previewTheme)
titleView.title = CounterControllerTitle(title: themeName, counter: hasInstallsCount ? strongSelf.presentationData.strings.Theme_UsersCount(max(1, theme.installCount ?? 0)) : "")
strongSelf.navigationItem.titleView = titleView
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationTheme: presentationTheme, presentationStrings: strongSelf.presentationData.strings))
}
})
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
strongSelf.presentationData = presentationData
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
self.disposable?.dispose()
self.applyDisposable.dispose()
}
@objc private func cancelPressed() {
self.dismiss(animated: true)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn()
}
}
}
override public func loadDisplayNode() {
super.loadDisplayNode()
var isPreview = false
var forceReady = false
var initialWallpaper: TelegramWallpaper?
if case let .settings(_, currentWallpaper, preview) = self.source {
isPreview = preview
forceReady = true
if let wallpaper = currentWallpaper {
initialWallpaper = wallpaper
}
}
self.displayNode = ThemePreviewControllerNode(context: self.context, previewTheme: self.previewTheme, initialWallpaper: initialWallpaper, dismiss: { [weak self] in
if let strongSelf = self {
strongSelf.dismiss()
}
}, apply: { [weak self] in
if let strongSelf = self {
strongSelf.apply()
}
}, isPreview: isPreview, forceReady: forceReady, ready: self._ready)
self.displayNodeDidLoad()
let previewTheme = self.previewTheme
if let initialWallpaper = initialWallpaper {
self.controllerNode.wallpaperPromise.set(.single(initialWallpaper))
} else if case let .file(file) = previewTheme.chat.defaultWallpaper, file.id == 0 {
self.controllerNode.wallpaperPromise.set(cachedWallpaper(account: self.context.account, slug: file.slug, settings: file.settings)
|> mapToSignal { wallpaper in
return .single(wallpaper?.wallpaper ?? .color(previewTheme.chatList.backgroundColor.argb))
})
} else {
self.controllerNode.wallpaperPromise.set(.single(previewTheme.chat.defaultWallpaper))
}
}
private func apply() {
if let customApply = self.customApply {
customApply()
Queue.mainQueue().after(0.2) {
self.dismiss()
}
return
}
let previewTheme = self.previewTheme
let theme: Signal<PresentationThemeReference?, NoError>
let context = self.context
let wallpaperPromise = self.controllerNode.wallpaperPromise
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let disposable = self.applyDisposable
switch self.source {
case let .settings(reference, _, _):
theme = .single(reference)
case .theme, .slug, .themeSettings:
theme = combineLatest(self.theme.get() |> take(1), wallpaperPromise.get() |> take(1))
|> mapToSignal { theme, wallpaper -> Signal<PresentationThemeReference?, NoError> in
if let theme = theme {
if case let .file(file) = wallpaper, file.id != 0 {
return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil)))
} else {
return .single(.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: theme.isCreator ? context.account.id : nil)))
}
} else {
return .complete()
}
}
case .media:
if let strings = encodePresentationTheme(previewTheme), let data = strings.data(using: .utf8) {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data)
theme = .single(.local(PresentationLocalTheme(title: previewTheme.name.string, resource: resource, resolvedWallpaper: nil)))
} else {
theme = .single(.builtin(.dayClassic))
}
}
var resolvedWallpaper: TelegramWallpaper?
let setup = theme
|> mapToSignal { theme -> Signal<(PresentationThemeReference, Bool), NoError> in
guard let theme = theme else {
return .complete()
}
switch theme {
case let .cloud(info):
resolvedWallpaper = info.resolvedWallpaper
return telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager)
|> take(1)
|> map { themes -> Bool in
if let _ = themes.first(where: { $0.id == info.theme.id }) {
return true
} else {
return false
}
}
|> map { exists in
return (theme, exists)
}
case let .local(info):
return wallpaperPromise.get()
|> take(1)
|> mapToSignal { currentWallpaper -> Signal<(PresentationThemeReference, Bool), NoError> in
if case let .file(file) = currentWallpaper, file.id != 0 {
resolvedWallpaper = currentWallpaper
}
var wallpaperImage: UIImage?
if case .file = currentWallpaper {
wallpaperImage = chatControllerBackgroundImage(theme: previewTheme, wallpaper: currentWallpaper, mediaBox: context.sharedContext.accountManager.mediaBox, knockoutMode: false)
}
let themeThumbnail = generateImage(CGSize(width: 213, height: 320.0), contextGenerator: { size, context in
if let image = generateImage(CGSize(width: 194.0, height: 291.0), contextGenerator: { size, c in
drawThemeImage(context: c, theme: previewTheme, wallpaperImage: wallpaperImage, size: size)
})?.cgImage {
context.draw(image, in: CGRect(origin: CGPoint(), size: size))
}
}, scale: 1.0)
let themeThumbnailData = themeThumbnail?.jpegData(compressionQuality: 0.6)
return telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager)
|> take(1)
|> mapToSignal { themes -> Signal<(PresentationThemeReference, Bool), NoError> in
let similarTheme = themes.first(where: { $0.isCreator && $0.title == info.title })
if let similarTheme = similarTheme {
return updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: similarTheme, title: nil, slug: nil, resource: info.resource, thumbnailData: themeThumbnailData, settings: nil)
|> map(Optional.init)
|> `catch` { _ -> Signal<CreateThemeResult?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(PresentationThemeReference, Bool), NoError> in
guard let result = result else {
let updatedTheme = PresentationLocalTheme(title: info.title, resource: info.resource, resolvedWallpaper: resolvedWallpaper)
return .single((.local(updatedTheme), true))
}
if case let .result(theme) = result, let file = theme.file {
context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id)
return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil)), true))
} else {
return .complete()
}
}
} else {
return createTheme(account: context.account, title: info.title, resource: info.resource, thumbnailData: themeThumbnailData, settings: nil)
|> map(Optional.init)
|> `catch` { _ -> Signal<CreateThemeResult?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<(PresentationThemeReference, Bool), NoError> in
guard let result = result else {
let updatedTheme = PresentationLocalTheme(title: info.title, resource: info.resource, resolvedWallpaper: resolvedWallpaper)
return .single((.local(updatedTheme), true))
}
if case let .result(updatedTheme) = result, let file = updatedTheme.file {
context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id)
return .single((.cloud(PresentationCloudTheme(theme: updatedTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: updatedTheme.isCreator ? context.account.id : nil)), true))
} else {
return .complete()
}
}
}
}
}
case .builtin:
return .single((theme, true))
}
}
|> mapToSignal { updatedTheme, existing -> Signal<(PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)?, NoError> in
if case let .cloud(info) = updatedTheme {
let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: info.theme).start()
if info.theme.emoticon == nil {
let _ = saveThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme).start()
}
}
let autoNightModeTriggered = context.sharedContext.currentPresentationData.with { $0 }.autoNightModeTriggered
return context.sharedContext.accountManager.transaction { transaction -> (PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)? in
var previousDefaultTheme: (PresentationThemeReference, PresentationThemeAccentColor?, Bool, PresentationThemeReference, Bool)?
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { entry in
let currentSettings: PresentationThemeSettings
if let entry = entry?.get(PresentationThemeSettings.self) {
currentSettings = entry
} else {
currentSettings = PresentationThemeSettings.defaultSettings
}
var updatedSettings: PresentationThemeSettings
if autoNightModeTriggered {
if case .builtin = currentSettings.automaticThemeSwitchSetting.theme {
previousDefaultTheme = (currentSettings.automaticThemeSwitchSetting.theme, currentSettings.themeSpecificAccentColors[currentSettings.automaticThemeSwitchSetting.theme.index], true, updatedTheme, existing)
}
var automaticThemeSwitchSetting = currentSettings.automaticThemeSwitchSetting
automaticThemeSwitchSetting.theme = updatedTheme
updatedSettings = currentSettings.withUpdatedAutomaticThemeSwitchSetting(automaticThemeSwitchSetting)
} else {
if case .builtin = currentSettings.theme {
previousDefaultTheme = (currentSettings.theme, currentSettings.themeSpecificAccentColors[currentSettings.theme.index], false, updatedTheme, existing)
}
updatedSettings = currentSettings.withUpdatedTheme(updatedTheme)
}
var themeSpecificAccentColors = updatedSettings.themeSpecificAccentColors
if case let .cloud(info) = updatedTheme, let settings = info.theme.settings?.first {
let baseThemeReference = PresentationThemeReference.builtin(PresentationBuiltinThemeReference(baseTheme: settings.baseTheme))
themeSpecificAccentColors[baseThemeReference.index] = PresentationThemeAccentColor(themeIndex: updatedTheme.index)
}
var themeSpecificChatWallpapers = updatedSettings.themeSpecificChatWallpapers
themeSpecificChatWallpapers[updatedTheme.index] = nil
return PreferencesEntry(updatedSettings.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers).withUpdatedThemeSpecificAccentColors(themeSpecificAccentColors))
})
return previousDefaultTheme
}
}
var cancelImpl: (() -> Void)?
let progress = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
self?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.35, queue: Queue.mainQueue())
let progressDisposable = progress.start()
cancelImpl = {
disposable.set(nil)
}
disposable.set((setup
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
|> deliverOnMainQueue).start(next: { [weak self] previousDefaultTheme in
if let strongSelf = self, let layout = strongSelf.validLayout {
Queue.mainQueue().after(0.3) {
if case .settings = strongSelf.source {
} else if layout.size.width >= 375.0 {
let navigationController = strongSelf.navigationController as? NavigationController
if let (previousDefaultTheme, previousAccentColor, autoNightMode, theme, _) = previousDefaultTheme {
let _ = (ApplicationSpecificNotice.getThemeChangeTip(accountManager: strongSelf.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] displayed in
guard let strongSelf = self, !displayed else {
return
}
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .actionSucceeded(title: strongSelf.presentationData.strings.Theme_ThemeChanged, text: strongSelf.presentationData.strings.Theme_ThemeChangedText, cancel: strongSelf.presentationData.strings.Undo_Undo, destructive: false), elevatedLayout: true, animateInAsReplacement: false, action: { value in
if value == .undo {
Queue.mainQueue().after(0.2) {
let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current -> PresentationThemeSettings in
var updated: PresentationThemeSettings
if autoNightMode {
var automaticThemeSwitchSetting = current.automaticThemeSwitchSetting
automaticThemeSwitchSetting.theme = previousDefaultTheme
updated = current.withUpdatedAutomaticThemeSwitchSetting(automaticThemeSwitchSetting)
} else {
updated = current.withUpdatedTheme(previousDefaultTheme)
}
var themeSpecificAccentColors = current.themeSpecificAccentColors
themeSpecificAccentColors[previousDefaultTheme.index] = previousAccentColor
updated = updated.withUpdatedThemeSpecificAccentColors(themeSpecificAccentColors)
return updated
}).start()
}
if case let .cloud(info) = theme {
let _ = deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: info.theme).start()
}
return true
} else if value == .info {
let controller = themeSettingsController(context: context)
controller.navigationPresentation = .modal
navigationController?.pushViewController(controller, animated: true)
return true
}
return false
}), in: .window(.root))
ApplicationSpecificNotice.markThemeChangeTipAsSeen(accountManager: strongSelf.context.sharedContext.accountManager)
})
}
}
strongSelf.dismiss()
}
}
}))
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
@objc private func actionPressed() {
let subject: ShareControllerSubject
let preferredAction: ShareControllerPreferredAction
switch self.source {
case .settings:
return
case let .theme(theme):
subject = .url("https://t.me/addtheme/\(theme.slug)")
preferredAction = .default
case let .slug(slug, _), let .themeSettings(slug, _):
subject = .url("https://t.me/addtheme/\(slug)")
preferredAction = .default
case let .media(media):
subject = .media(media, nil)
preferredAction = .default
}
let controller = ShareController(context: self.context, subject: subject, preferredAction: preferredAction)
self.present(controller, in: .window(.root), blockInteraction: true)
}
}
@@ -0,0 +1,757 @@
import Foundation
import UIKit
import Display
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import ChatListUI
import WallpaperResources
import LegacyComponents
import WallpaperBackgroundNode
import AnimationCache
import MultiAnimationRenderer
import WallpaperGalleryScreen
private func generateMaskImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [color.withAlphaComponent(0.0).cgColor, color.cgColor, color.cgColor] as CFArray
var locations: [CGFloat] = [0.0, 0.75, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: 80.0), options: CGGradientDrawingOptions())
})
}
final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate {
private let context: AccountContext
private var previewTheme: PresentationTheme
private var presentationData: PresentationData
private let isPreview: Bool
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private let ready: Promise<Bool>
public let wallpaperPromise = Promise<TelegramWallpaper>()
private let referenceTimestamp: Int32
private let scrollNode: ASScrollNode
private let pageControlBackgroundNode: ASDisplayNode
private let pageControlNode: PageControlNode
private let chatListBackgroundNode: ASDisplayNode
private var chatNodes: [ListViewItemNode]?
private let maskNode: ASImageNode
private let separatorNode: ASDisplayNode
private let chatContainerNode: ASDisplayNode
private let messagesContainerNode: ASDisplayNode
private let instantChatBackgroundNode: WallpaperBackgroundNode
private let remoteChatBackgroundNode: TransformImageNode
private let blurredNode: BlurredImageNode
private let wallpaperNode: WallpaperBackgroundNode
private var dateHeaderNode: ListViewItemHeaderNode?
private var messageNodes: [ListViewItemNode]?
private let toolbarNode: WallpaperGalleryToolbarNode
private var validLayout: (ContainerViewLayout, CGFloat)?
private var wallpaperDisposable: Disposable?
private var colorDisposable: Disposable?
private var statusDisposable: Disposable?
private var fetchDisposable = MetaDisposable()
private var dismissed = false
private var wallpaper: TelegramWallpaper
init(context: AccountContext, previewTheme: PresentationTheme, initialWallpaper: TelegramWallpaper?, dismiss: @escaping () -> Void, apply: @escaping () -> Void, isPreview: Bool, forceReady: Bool, ready: Promise<Bool>) {
self.context = context
self.previewTheme = previewTheme
self.isPreview = isPreview
self.wallpaper = initialWallpaper ?? previewTheme.chat.defaultWallpaper
self.ready = ready
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.animationCache = context.animationCache
self.animationRenderer = context.animationRenderer
let calendar = Calendar(identifier: .gregorian)
var components = calendar.dateComponents(Set([.era, .year, .month, .day, .hour, .minute, .second]), from: Date())
components.hour = 13
components.minute = 0
components.second = 0
self.referenceTimestamp = Int32(calendar.date(from: components)?.timeIntervalSince1970 ?? 0.0)
self.scrollNode = ASScrollNode()
self.pageControlBackgroundNode = ASDisplayNode()
self.pageControlBackgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3)
self.pageControlBackgroundNode.cornerRadius = 10.5
self.pageControlNode = PageControlNode(dotSpacing: 7.0, dotColor: .white, inactiveDotColor: UIColor.white.withAlphaComponent(0.4))
self.chatListBackgroundNode = ASDisplayNode()
self.chatContainerNode = ASDisplayNode()
self.chatContainerNode.clipsToBounds = true
self.messagesContainerNode = ASDisplayNode()
self.messagesContainerNode.clipsToBounds = true
self.messagesContainerNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.instantChatBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
self.instantChatBackgroundNode.displaysAsynchronously = false
self.ready.set(.single(true))
self.instantChatBackgroundNode.update(wallpaper: wallpaper, animated: false)
self.instantChatBackgroundNode.view.contentMode = .scaleAspectFill
self.remoteChatBackgroundNode = TransformImageNode()
self.remoteChatBackgroundNode.view.contentMode = .scaleAspectFill
self.blurredNode = BlurredImageNode()
self.blurredNode.blurView.contentMode = .scaleAspectFill
self.wallpaperNode = createWallpaperBackgroundNode(context: context, forChatDisplay: false)
self.toolbarNode = WallpaperGalleryToolbarNode(theme: self.previewTheme, strings: self.presentationData.strings, doneButtonType: .set)
if case .file = previewTheme.chat.defaultWallpaper, !forceReady {
self.toolbarNode.setDoneEnabled(false)
}
self.maskNode = ASImageNode()
self.maskNode.displaysAsynchronously = false
self.maskNode.displayWithoutProcessing = true
self.maskNode.contentMode = .scaleToFill
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = previewTheme.rootController.tabBar.separatorColor
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.backgroundColor = self.previewTheme.list.plainBackgroundColor
self.chatListBackgroundNode.backgroundColor = self.previewTheme.chatList.backgroundColor
self.maskNode.image = generateMaskImage(color: self.previewTheme.chatList.backgroundColor)
if case let .color(value) = self.wallpaper {
self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value)
}
self.pageControlNode.isUserInteractionEnabled = false
self.pageControlNode.pagesCount = 2
self.addSubnode(self.scrollNode)
if !isPreview {
self.chatListBackgroundNode.addSubnode(self.maskNode)
self.addSubnode(self.pageControlBackgroundNode)
self.addSubnode(self.pageControlNode)
self.addSubnode(self.toolbarNode)
}
self.scrollNode.addSubnode(self.chatListBackgroundNode)
self.scrollNode.addSubnode(self.chatContainerNode)
self.chatContainerNode.addSubnode(self.instantChatBackgroundNode)
self.chatContainerNode.addSubnode(self.remoteChatBackgroundNode)
self.chatContainerNode.addSubnode(self.messagesContainerNode)
self.addSubnode(self.separatorNode)
self.toolbarNode.cancel = {
dismiss()
}
self.toolbarNode.done = { [weak self] _ in
if let strongSelf = self {
if !strongSelf.dismissed {
strongSelf.dismissed = true
apply()
}
}
}
var gradientColors: [UInt32] = []
if case let .file(file) = self.wallpaper {
gradientColors = file.settings.colors
if file.settings.blur {
self.chatContainerNode.insertSubnode(self.blurredNode, belowSubnode: self.messagesContainerNode)
}
} else if case let .gradient(gradient) = self.wallpaper {
gradientColors = gradient.colors
}
if gradientColors.count >= 3 {
self.chatContainerNode.insertSubnode(self.wallpaperNode, belowSubnode: self.messagesContainerNode)
}
self.wallpaperNode.update(wallpaper: self.wallpaper, animated: false)
self.wallpaperNode.updateBubbleTheme(bubbleTheme: self.previewTheme, bubbleCorners: self.presentationData.chatBubbleCorners)
self.remoteChatBackgroundNode.imageUpdated = { [weak self] image in
if let strongSelf = self, strongSelf.blurredNode.supernode != nil {
var image = image
if let imageToScale = image {
let actualSize = CGSize(width: imageToScale.size.width * imageToScale.scale, height: imageToScale.size.height * imageToScale.scale)
if actualSize.width > 1280.0 || actualSize.height > 1280.0 {
image = TGScaleImageToPixelSize(image, actualSize.fitted(CGSize(width: 1280.0, height: 1280.0)))
}
}
strongSelf.blurredNode.image = image
strongSelf.blurredNode.blurView.blurRadius = 45.0
}
self?.ready.set(.single(true))
}
self.colorDisposable = (self.wallpaperPromise.get()
|> mapToSignal { wallpaper -> Signal<UIColor, NoError> in
if case let .file(file) = wallpaper, file.id == 0 {
return .complete()
} else {
return chatServiceBackgroundColor(wallpaper: wallpaper, mediaBox: context.account.postbox.mediaBox)
}
}
|> deliverOnMainQueue).start(next: { [weak self] color in
if let strongSelf = self {
strongSelf.pageControlBackgroundNode.backgroundColor = color
}
})
self.wallpaperDisposable = (self.wallpaperPromise.get()
|> deliverOnMainQueue).start(next: { [weak self] wallpaper in
guard let strongSelf = self else {
return
}
var useDarkButton = true
if case let .file(file) = wallpaper {
let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100)
let displaySize = dimensions.cgSize.dividedByScreenScale().integralFloor
var convertedRepresentations: [ImageRepresentationWithReference] = []
for representation in file.file.previewRepresentations {
convertedRepresentations.append(ImageRepresentationWithReference(representation: representation, reference: .wallpaper(wallpaper: .slug(file.slug), resource: representation.resource)))
}
convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)))
let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
if wallpaper.isPattern {
signal = .complete()
} else {
useDarkButton = false
signal = .complete()
}
strongSelf.remoteChatBackgroundNode.setSignal(signal)
strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.sharedContext.accountManager.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start())
let account = strongSelf.context.account
let statusSignal = strongSelf.context.sharedContext.accountManager.mediaBox.resourceStatus(file.file.resource)
|> take(1)
|> mapToSignal { status -> Signal<MediaResourceStatus, NoError> in
if case .Local = status {
return .single(status)
} else {
return account.postbox.mediaBox.resourceStatus(file.file.resource)
}
}
strongSelf.statusDisposable = (statusSignal
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self, case .Local = status {
strongSelf.toolbarNode.setDoneEnabled(true)
}
})
var patternArguments: PatternWallpaperArguments?
if !file.settings.colors.isEmpty {
var patternIntensity: CGFloat = 0.5
if let intensity = file.settings.intensity {
patternIntensity = CGFloat(intensity) / 100.0
}
var patternColors = [UIColor(rgb: file.settings.colors[0], alpha: patternIntensity)]
if file.settings.colors.count >= 2 {
patternColors.append(UIColor(rgb: file.settings.colors[1], alpha: patternIntensity))
}
patternArguments = PatternWallpaperArguments(colors: patternColors, rotation: file.settings.rotation)
}
strongSelf.remoteChatBackgroundNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets(), custom: patternArguments))()
strongSelf.toolbarNode.dark = useDarkButton
}
})
}
deinit {
self.colorDisposable?.dispose()
self.wallpaperDisposable?.dispose()
self.statusDisposable?.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.bounces = false
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.isPagingEnabled = true
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.pageControlNode.setPage(0.0)
}
func updateTheme(_ theme: PresentationTheme) {
self.previewTheme = theme
self.backgroundColor = self.previewTheme.list.plainBackgroundColor
self.chatListBackgroundNode.backgroundColor = self.previewTheme.chatList.backgroundColor
self.maskNode.image = generateMaskImage(color: self.previewTheme.chatList.backgroundColor)
if case let .color(value) = self.wallpaper {
self.instantChatBackgroundNode.backgroundColor = UIColor(rgb: value)
}
self.toolbarNode.updateThemeAndStrings(theme: self.previewTheme, strings: self.presentationData.strings)
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bounds = scrollView.bounds
if !bounds.width.isZero {
self.pageControlNode.setPage(scrollView.contentOffset.x / bounds.width)
}
}
func animateIn(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
}
func animateOut(completion: (() -> Void)? = nil) {
if let (layout, _) = self.validLayout, case .compact = layout.metrics.widthClass {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
} else {
completion?()
}
}
private func updateChatsLayout(layout: ContainerViewLayout, topInset: CGFloat, transition: ContainedViewLayoutTransition) {
var items: [ChatListItem] = []
let interaction = ChatListNodeInteraction(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
}, openStories: { _, _ in
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
}, openAdInfo: { _, _ in
}, openAccountFreezeInfo: {
}, openUrl: { _ in
})
func makeChatListItem(
peer: EnginePeer,
author: EnginePeer,
timestamp: Int32,
text: String,
isPinned: Bool = false,
presenceTimestamp: Int32? = nil,
hasInputActivity: Bool = false,
unreadCount: Int32 = 0
) -> ChatListItem {
return ChatListItem(
presentationData: chatListPresentationData,
context: self.context,
chatListLocation: .chatList(groupId: .root),
filterData: nil,
index: .chatList(ChatListIndex(pinningIndex: isPinned ? 0 : nil, messageIndex: MessageIndex(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0), timestamp: timestamp))),
content: .peer(ChatListItemContent.PeerData(
messages: [
EngineMessage(
stableId: 0,
stableVersion: 0,
id: EngineMessage.Id(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: timestamp,
flags: author.id == peer.id ? [.Incoming] : [],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: author,
text: text,
attributes: [],
media: [],
peers: [:],
associatedMessages: [:],
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
],
peer: EngineRenderedPeer(peer: peer),
threadInfo: nil,
combinedReadState: EnginePeerReadCounters(incomingReadId: 1000, outgoingReadId: 1000, count: unreadCount, markedUnread: false),
isRemovedFromTotalUnreadCount: false,
presence: presenceTimestamp.flatMap { presenceTimestamp in
EnginePeer.Presence(status: .present(until: presenceTimestamp + 1000), lastActivity: presenceTimestamp)
},
hasUnseenMentions: false,
hasUnseenReactions: false,
draftState: nil,
mediaDraftContentType: nil,
inputActivities: hasInputActivity ? [(author, .typingText)] : [],
promoInfo: nil,
ignoreUnreadBadge: false,
displayAsMessage: false,
hasFailedMessages: false,
forumTopicData: nil,
topForumTopicItems: [],
autoremoveTimeout: nil,
storyState: nil,
requiresPremiumForMessaging: false,
displayAsTopicList: false,
tags: []
)),
editing: false,
hasActiveRevealControls: false,
selected: false,
header: nil,
enabledContextActions: nil,
hiddenOffset: false,
interaction: interaction
)
}
let chatListPresentationData = ChatListPresentationData(theme: self.previewTheme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
let selfPeer: EnginePeer = .user(TelegramUser(id: self.context.account.peerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer1: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer2: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_2_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer3: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(3)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil))
let peer3Author: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_AuthorName, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer4: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer5: EnginePeer = .channel(TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, title: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Name, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .broadcast(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil))
let peer6: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(5)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let peer7: EnginePeer = .user(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(6)), accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Name, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
let timestamp = self.referenceTimestamp
let timestamp1 = timestamp + 120
items.append(makeChatListItem(
peer: peer1,
author: selfPeer,
timestamp: timestamp1,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_1_Text
))
let presenceTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 60 * 60)
let timestamp2 = timestamp + 3660
items.append(makeChatListItem(
peer: peer2,
author: peer2,
timestamp: timestamp2,
text: "",
presenceTimestamp: presenceTimestamp,
hasInputActivity: true
))
let timestamp3 = timestamp + 3200
items.append(makeChatListItem(
peer: peer3,
author: peer3Author,
timestamp: timestamp3,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_3_Text
))
let timestamp4 = timestamp + 3000
items.append(makeChatListItem(
peer: peer4,
author: peer4,
timestamp: timestamp4,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_4_Text
))
let timestamp5 = timestamp + 1000
items.append(makeChatListItem(
peer: peer5,
author: peer5,
timestamp: timestamp5,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_5_Text
))
items.append(makeChatListItem(
peer: peer6,
author: peer6,
timestamp: timestamp - 360,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_6_Text
))
items.append(makeChatListItem(
peer: peer7,
author: peer6,
timestamp: timestamp - 420,
text: self.presentationData.strings.Appearance_ThemePreview_ChatList_7_Text
))
let width: CGFloat
if case .regular = layout.metrics.widthClass {
width = layout.size.width / 2.0
} else {
width = layout.size.width
}
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let chatNodes = self.chatNodes {
for i in 0 ..< items.count {
let itemNode = chatNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var chatNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
chatNodes.append(itemNode!)
if self.maskNode.supernode != nil {
self.chatListBackgroundNode.insertSubnode(itemNode!, belowSubnode: self.maskNode)
} else {
self.chatListBackgroundNode.addSubnode(itemNode!)
}
}
self.chatNodes = chatNodes
}
if let chatNodes = self.chatNodes {
var topOffset: CGFloat = topInset
for itemNode in chatNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: itemNode.frame.size))
topOffset += itemNode.frame.height
}
}
}
private func updateMessagesLayout(layout: ContainerViewLayout, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let headerItem = self.context.sharedContext.makeChatMessageDateHeaderItem(context: self.context, timestamp: self.referenceTimestamp, theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.presentationData.chatWallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder)
var items: [ListViewItem] = []
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
let otherPeerId = self.context.account.peerId
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
peers[otherPeerId] = TelegramUser(id: otherPeerId, accessHash: nil, firstName: self.presentationData.strings.Appearance_ThemePreview_Chat_2_ReplyName, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: .preset(.blue), backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
var sampleMessages: [Message] = []
let message1 = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_4_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message1)
let message2 = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 2), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66001, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_5_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message2)
let message3 = Message(stableId: 3, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 3), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66002, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_6_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message3)
let message4 = Message(stableId: 4, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 4), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66003, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_7_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages[message4.id] = message4
sampleMessages.append(message4)
let message5 = Message(stableId: 5, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 5), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66004, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_1_Text, attributes: [ReplyMessageAttribute(messageId: message4.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
messages[message5.id] = message5
sampleMessages.append(message5)
let waveformBase64 = "DAAOAAkACQAGAAwADwAMABAADQAPABsAGAALAA0AGAAfABoAHgATABgAGQAYABQADAAVABEAHwANAA0ACQAWABkACQAOAAwACQAfAAAAGQAVAAAAEwATAAAACAAfAAAAHAAAABwAHwAAABcAGQAAABQADgAAABQAHwAAAB8AHwAAAAwADwAAAB8AEwAAABoAFwAAAB8AFAAAAAAAHwAAAAAAHgAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAHwAAAAAAAAA="
let voiceAttributes: [TelegramMediaFileAttribute] = [.Audio(isVoice: true, duration: 23, title: nil, performer: nil, waveform: Data(base64Encoded: waveformBase64)!)]
let voiceMedia = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: LocalFileMediaResource(fileId: 0), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: 0, attributes: voiceAttributes, alternativeRepresentations: [])
let message6 = Message(stableId: 6, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 6), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66005, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [voiceMedia], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message6)
let message7 = Message(stableId: 7, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 7), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66006, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_2_Text, attributes: [ReplyMessageAttribute(messageId: message5.id, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message7)
let message8 = Message(stableId: 8, stableVersion: 0, id: MessageId(peerId: otherPeerId, namespace: 0, id: 8), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66007, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[otherPeerId], text: self.presentationData.strings.Appearance_ThemePreview_Chat_3_Text, attributes: [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
sampleMessages.append(message8)
items = sampleMessages.reversed().map { message in
self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: [message], theme: self.previewTheme, strings: self.presentationData.strings, wallpaper: self.wallpaper, fontSize: self.presentationData.chatFontSize, chatBubbleCorners: self.presentationData.chatBubbleCorners, dateTimeFormat: self.presentationData.dateTimeFormat, nameOrder: self.presentationData.nameDisplayOrder, forcedResourceStatus: !message.media.isEmpty ? FileMediaResourceStatus(mediaStatus: .playbackStatus(.paused), fetchStatus: .Local) : nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)
}
let width: CGFloat
if case .regular = layout.metrics.widthClass {
width = layout.size.width / 2.0
} else {
width = layout.size.width
}
let params = ListViewItemLayoutParams(width: width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height)
if let messageNodes = self.messageNodes {
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
self.messagesContainerNode.addSubnode(itemNode!)
}
self.messageNodes = messageNodes
}
var bottomOffset: CGFloat = 9.0 + bottomInset
if let messageNodes = self.messageNodes {
for itemNode in messageNodes {
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: itemNode.frame.size))
bottomOffset += itemNode.frame.height
itemNode.updateFrame(itemNode.frame, within: layout.size)
}
}
let dateHeaderNode: ListViewItemHeaderNode
if let currentDateHeaderNode = self.dateHeaderNode {
dateHeaderNode = currentDateHeaderNode
headerItem.updateNode(dateHeaderNode, previous: nil, next: headerItem)
} else {
dateHeaderNode = headerItem.node(synchronousLoad: true)
dateHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.messagesContainerNode.addSubnode(dateHeaderNode)
self.dateHeaderNode = dateHeaderNode
}
transition.updateFrame(node: dateHeaderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: bottomOffset), size: CGSize(width: layout.size.width, height: headerItem.height)))
dateHeaderNode.updateLayout(size: self.messagesContainerNode.frame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
let bounds = CGRect(origin: CGPoint(), size: layout.size)
self.scrollNode.frame = bounds
let toolbarHeight = 49.0 + layout.intrinsicInsets.bottom
self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height)
self.chatContainerNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
let bottomInset: CGFloat
if case .regular = layout.metrics.widthClass {
self.chatListBackgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height)
self.chatContainerNode.frame = CGRect(x: bounds.width / 2.0, y: 0.0, width: bounds.width / 2.0, height: bounds.height)
self.scrollNode.view.contentSize = CGSize(width: bounds.width, height: bounds.height)
self.pageControlNode.isHidden = true
self.pageControlBackgroundNode.isHidden = true
self.separatorNode.isHidden = false
self.separatorNode.frame = CGRect(x: bounds.width / 2.0, y: 0.0, width: UIScreenPixel, height: bounds.height - toolbarHeight)
bottomInset = 0.0
} else {
self.chatListBackgroundNode.frame = CGRect(x: bounds.width, y: 0.0, width: bounds.width, height: bounds.height)
self.chatContainerNode.frame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
self.scrollNode.view.contentSize = CGSize(width: bounds.width * 2.0, height: bounds.height)
self.pageControlNode.isHidden = false
self.pageControlBackgroundNode.isHidden = false
self.separatorNode.isHidden = true
bottomInset = 38.0
}
self.messagesContainerNode.frame = self.chatContainerNode.bounds
self.instantChatBackgroundNode.frame = self.chatContainerNode.bounds
self.instantChatBackgroundNode.updateLayout(size: self.instantChatBackgroundNode.bounds.size, displayMode: .aspectFill, transition: .immediate)
self.remoteChatBackgroundNode.frame = self.chatContainerNode.bounds
self.blurredNode.frame = self.chatContainerNode.bounds
self.wallpaperNode.frame = self.chatContainerNode.bounds
self.wallpaperNode.updateLayout(size: self.wallpaperNode.bounds.size, displayMode: .aspectFill, transition: .immediate)
transition.updateFrame(node: self.toolbarNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight)))
self.toolbarNode.updateLayout(size: CGSize(width: layout.size.width, height: 49.0), layout: layout, transition: transition)
self.updateChatsLayout(layout: layout, topInset: navigationBarHeight, transition: transition)
self.updateMessagesLayout(layout: layout, bottomInset: self.isPreview ? 0.0 : (toolbarHeight + bottomInset), transition: transition)
let pageControlSize = self.pageControlNode.measure(CGSize(width: bounds.width, height: 100.0))
let pageControlFrame = CGRect(origin: CGPoint(x: floor((bounds.width - pageControlSize.width) / 2.0), y: layout.size.height - toolbarHeight - 28.0), size: pageControlSize)
self.pageControlNode.frame = pageControlFrame
self.pageControlBackgroundNode.frame = CGRect(x: pageControlFrame.minX - 7.0, y: pageControlFrame.minY - 7.0, width: pageControlFrame.width + 14.0, height: 21.0)
transition.updateFrame(node: self.maskNode, frame: CGRect(x: 0.0, y: layout.size.height - toolbarHeight - 80.0, width: bounds.width, height: 80.0))
}
}
@@ -0,0 +1,971 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import ItemListUI
import ContextUI
import PresentationDataUtils
private enum ThemeSettingsColorEntryId: Hashable {
case color(Int64)
case theme(Int64)
case picker
}
private enum ThemeSettingsColorEntry: Comparable, Identifiable {
case color(Int, PresentationTheme, PresentationThemeReference, PresentationThemeAccentColor?, Bool)
case theme(Int, PresentationTheme, PresentationThemeReference, PresentationThemeReference, Bool)
case picker
var stableId: ThemeSettingsColorEntryId {
switch self {
case let .color(_, _, themeReference, accentColor, _):
return .color(themeReference.index &+ Int64(accentColor?.index ?? 0))
case let .theme(_, _, _, theme, _):
return .theme(theme.index)
case .picker:
return .picker
}
}
static func ==(lhs: ThemeSettingsColorEntry, rhs: ThemeSettingsColorEntry) -> Bool {
switch lhs {
case let .color(lhsIndex, lhsCurrentTheme, lhsThemeReference, lhsAccentColor, lhsSelected):
if case let .color(rhsIndex, rhsCurrentTheme, rhsThemeReference, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCurrentTheme === rhsCurrentTheme, lhsThemeReference.index == rhsThemeReference.index, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected {
return true
} else {
return false
}
case let .theme(lhsIndex, lhsCurrentTheme, lhsBaseThemeReference, lhsTheme, lhsSelected):
if case let .theme(rhsIndex, rhsCurrentTheme, rhsBaseThemeReference, rhsTheme, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCurrentTheme === rhsCurrentTheme, lhsBaseThemeReference.index == rhsBaseThemeReference.index, lhsTheme == rhsTheme, lhsSelected == rhsSelected {
return true
} else {
return false
}
case .picker:
if case .picker = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: ThemeSettingsColorEntry, rhs: ThemeSettingsColorEntry) -> Bool {
switch lhs {
case .picker:
return true
case let .color(lhsIndex, _, _, _, _), let .theme(lhsIndex, _, _, _, _):
switch rhs {
case let .color(rhsIndex, _, _, _, _):
return lhsIndex < rhsIndex
case let .theme(rhsIndex, _, _, _, _):
return lhsIndex < rhsIndex
case .picker:
return false
}
}
}
func item(action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void) -> ListViewItem {
switch self {
case let .color(_, currentTheme, themeReference, accentColor, selected):
return ThemeSettingsAccentColorIconItem(themeReference: themeReference, theme: currentTheme, color: accentColor.flatMap { .accentColor($0) }, selected: selected, action: action, contextAction: contextAction)
case let .theme(_, currentTheme, baseThemeReference, theme, selected):
return ThemeSettingsAccentColorIconItem(themeReference: baseThemeReference, theme: currentTheme, color: .theme(theme), selected: selected, action: action, contextAction: contextAction)
case .picker:
return ThemeSettingsAccentColorPickerItem(action: openColorPicker)
}
}
}
enum ThemeSettingsColorOption: Equatable {
case accentColor(PresentationThemeAccentColor)
case theme(PresentationThemeReference)
var accentColor: UIColor? {
switch self {
case let .accentColor(color):
return color.color
case let .theme(reference):
if case let .cloud(theme) = reference, let settings = theme.theme.settings?.first {
return UIColor(argb: settings.accentColor)
} else {
return nil
}
}
}
var baseColor: UIColor? {
switch self {
case let .accentColor(color):
return color.baseColor.color
case .theme:
return .clear
}
}
var plainBubbleColors: [UInt32] {
switch self {
case let .accentColor(color):
return color.plainBubbleColors
case let .theme(reference):
if case let .cloud(theme) = reference, let settings = theme.theme.settings?.first, !settings.messageColors.isEmpty {
return settings.messageColors
} else {
return []
}
}
}
var customBubbleColors: [UInt32] {
switch self {
case let .accentColor(color):
return color.customBubbleColors
case let .theme(reference):
if case let .cloud(theme) = reference, let settings = theme.theme.settings?.first, !settings.messageColors.isEmpty {
return settings.messageColors
} else {
return []
}
}
}
var wallpaper: TelegramWallpaper? {
switch self {
case let .accentColor(color):
return color.wallpaper
case .theme:
return nil
}
}
var index: Int64 {
switch self {
case let .accentColor(color):
return Int64(color.index)
case let .theme(reference):
return reference.index
}
}
}
private class ThemeSettingsAccentColorIconItem: ListViewItem {
let themeReference: PresentationThemeReference
let theme: PresentationTheme
let color: ThemeSettingsColorOption?
let selected: Bool
let action: (ThemeSettingsColorOption?, Bool) -> Void
let contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?
public init(themeReference: PresentationThemeReference, theme: PresentationTheme, color: ThemeSettingsColorOption?, selected: Bool, action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?) {
self.themeReference = themeReference
self.theme = theme
self.color = color
self.selected = selected
self.action = action
self.contextAction = contextAction
}
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 = ThemeSettingsAccentColorIconItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
}
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 {
assert(node() is ThemeSettingsAccentColorIconItemNode)
if let nodeValue = node() as? ThemeSettingsAccentColorIconItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
let animated: Bool
if case .Crossfade = animation {
animated = true
} else {
animated = false
}
apply(animated)
})
}
}
}
}
}
public var selectable = true
public func selected(listView: ListView) {
self.action(self.color, self.selected)
}
}
private func generateRingImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setStrokeColor(color.cgColor)
context.setLineWidth(2.0)
context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0))
})
}
private func generateFillImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(color.cgColor)
context.fillEllipse(in: bounds.insetBy(dx: 4.0, dy: 4.0))
})
}
private func generateCenterImage(topColor: UIColor, bottomColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.addEllipse(in: bounds.insetBy(dx: 10.0, dy: 10.0))
context.clip()
let gradientColors = [topColor.cgColor, bottomColor.cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 10.0), end: CGPoint(x: 0.0, y: size.height - 10.0), options: CGGradientDrawingOptions())
})
}
private func generateDotsImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(UIColor.white.cgColor)
let dotSize = CGSize(width: 4.0, height: 4.0)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 11.0, y: 18.0), size: dotSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 18.0, y: 18.0), size: dotSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: 25.0, y: 18.0), size: dotSize))
})
}
private final class ThemeSettingsAccentColorIconItemNode : ListViewItemNode {
private let containerNode: ContextControllerSourceNode
private let fillNode: ASImageNode
private let ringNode: ASImageNode
private let centerNode: ASImageNode
private let dotsNode: ASImageNode
var item: ThemeSettingsAccentColorIconItem?
init() {
self.containerNode = ContextControllerSourceNode()
self.fillNode = ASImageNode()
self.fillNode.displaysAsynchronously = false
self.fillNode.displayWithoutProcessing = true
self.ringNode = ASImageNode()
self.ringNode.displaysAsynchronously = false
self.ringNode.displayWithoutProcessing = true
self.centerNode = ASImageNode()
self.centerNode.displaysAsynchronously = false
self.centerNode.displayWithoutProcessing = true
self.dotsNode = ASImageNode()
self.dotsNode.displaysAsynchronously = false
self.dotsNode.displayWithoutProcessing = true
self.dotsNode.image = generateDotsImage()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.fillNode)
self.containerNode.addSubnode(self.ringNode)
self.containerNode.addSubnode(self.dotsNode)
self.containerNode.addSubnode(self.centerNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item else {
gesture.cancel()
return
}
item.contextAction?(item.color, item.selected, strongSelf.containerNode, gesture)
}
}
override func didLoad() {
super.didLoad()
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
func setSelected(_ selected: Bool, animated: Bool = false) {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate
if selected {
transition.updateTransformScale(node: self.fillNode, scale: 1.0)
transition.updateTransformScale(node: self.centerNode, scale: 0.16)
transition.updateAlpha(node: self.centerNode, alpha: 0.0)
transition.updateTransformScale(node: self.dotsNode, scale: 1.0)
transition.updateAlpha(node: self.dotsNode, alpha: 1.0)
} else {
transition.updateTransformScale(node: self.fillNode, scale: 1.2)
transition.updateTransformScale(node: self.centerNode, scale: 1.0)
transition.updateAlpha(node: self.centerNode, alpha: 1.0)
transition.updateTransformScale(node: self.dotsNode, scale: 0.85)
transition.updateAlpha(node: self.dotsNode, alpha: 0.0)
}
}
func asyncLayout() -> (ThemeSettingsAccentColorIconItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let currentItem = self.item
return { [weak self] item, params in
var updatedAccentColor = false
var updatedSelected = false
if currentItem == nil || currentItem?.color != item.color || currentItem?.themeReference != item.themeReference {
updatedAccentColor = true
}
if currentItem?.selected != item.selected {
updatedSelected = true
}
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 58.0), insets: UIEdgeInsets())
return (itemLayout, { animated in
if let strongSelf = self {
strongSelf.item = item
if updatedAccentColor {
var fillColor = item.color?.accentColor
var strokeColor = item.color?.baseColor
if strokeColor == .clear {
strokeColor = fillColor
}
if let color = strokeColor, color.distance(to: item.theme.list.itemBlocksBackgroundColor) < 200 {
if color.distance(to: UIColor.white) < 200 {
strokeColor = UIColor(rgb: 0x999999)
} else {
strokeColor = item.theme.list.controlSecondaryColor
}
}
var topColor: UIColor?
var bottomColor: UIColor?
if let colors = item.color?.plainBubbleColors, !colors.isEmpty {
topColor = UIColor(rgb: colors[0])
bottomColor = UIColor(rgb: colors.last ?? colors[0])
} else if case .builtin(.dayClassic) = item.themeReference {
if let accentColor = item.color?.accentColor {
let hsb = accentColor.hsb
let bubbleColor = UIColor(hue: hsb.0, saturation: (hsb.1 > 0.0 && hsb.2 > 0.0) ? 0.14 : 0.0, brightness: 0.79 + hsb.2 * 0.21, alpha: 1.0)
topColor = bubbleColor
bottomColor = bubbleColor
} else {
fillColor = UIColor(rgb: 0x0088ff)
strokeColor = fillColor
topColor = UIColor(rgb: 0xe1ffc7)
bottomColor = topColor
}
} else if case .builtin(.nightAccent) = item.themeReference {
if let accentColor = item.color?.accentColor {
bottomColor = accentColor.withMultiplied(hue: 1.019, saturation: 0.731, brightness: 0.59)
topColor = bottomColor!.withMultiplied(hue: 0.966, saturation: 0.61, brightness: 0.98)
} else {
fillColor = UIColor(rgb: 0x2ea6ff)
strokeColor = fillColor
topColor = UIColor(rgb: 0x466f95)
bottomColor = topColor
}
}
strongSelf.fillNode.image = generateFillImage(color: fillColor ?? .clear)
strongSelf.ringNode.image = generateRingImage(color: strokeColor ?? .clear)
strongSelf.centerNode.image = generateCenterImage(topColor: topColor ?? .clear, bottomColor: bottomColor ?? .clear)
}
let center = CGPoint(x: 30.0, y: 29.0)
let bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 40.0, height: 40.0))
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize)
strongSelf.fillNode.position = center
strongSelf.ringNode.position = center
strongSelf.centerNode.position = center
strongSelf.dotsNode.position = center
strongSelf.fillNode.bounds = bounds
strongSelf.ringNode.bounds = bounds
strongSelf.centerNode.bounds = bounds
strongSelf.dotsNode.bounds = bounds
if updatedSelected {
strongSelf.setSelected(item.selected, animated: !updatedAccentColor && currentItem != nil)
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
private class ThemeSettingsAccentColorPickerItem: ListViewItem {
let action: (Bool) -> Void
public init(action: @escaping (Bool) -> Void) {
self.action = action
}
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 = ThemeSettingsAccentColorPickerItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
}
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 {
assert(node() is ThemeSettingsAccentColorPickerItemNode)
if let nodeValue = node() as? ThemeSettingsAccentColorPickerItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(animation.isAnimated)
})
}
}
}
}
}
public var selectable = true
public func selected(listView: ListView) {
self.action(true)
}
}
private func generateCustomSwatchImage() -> UIImage? {
return generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let dotSize = CGSize(width: 10.0, height: 10.0)
context.setFillColor(UIColor(rgb: 0xd33213).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 16.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0xf08200).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0xedb400).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 28.0, y: 8.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0x70bb23).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 28.0, y: 24.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0x5396fa).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 32.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0x9472ee).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 24.0), size: dotSize))
context.setFillColor(UIColor(rgb: 0xeb6ca4).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: dotSize))
})
}
private final class ThemeSettingsAccentColorPickerItemNode : ListViewItemNode {
private let imageNode: ASImageNode
var item: ThemeSettingsAccentColorPickerItem?
init() {
self.imageNode = ASImageNode()
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.image = generateCustomSwatchImage()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.imageNode)
}
override func didLoad() {
super.didLoad()
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
func asyncLayout() -> (ThemeSettingsAccentColorPickerItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
return { [weak self] item, params in
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 60.0, height: 60.0), insets: UIEdgeInsets())
return (itemLayout, { animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 11.0, y: 9.0), size: CGSize(width: 42.0, height: 42.0))
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
enum ThemeSettingsAccentColor {
case `default`
case color(PresentationThemeBaseColor)
case preset(PresentationThemeAccentColor)
case custom(PresentationThemeAccentColor)
case theme(PresentationThemeReference)
var index: Int64? {
switch self {
case .default:
return nil
case let .color(color):
return Int64(10 + color.rawValue)
case let .preset(color), let .custom(color):
return Int64(color.index)
case let .theme(theme):
return theme.index
}
}
}
class ThemeSettingsAccentColorItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let theme: PresentationTheme
let generalThemeReference: PresentationThemeReference
let themeReference: PresentationThemeReference
let colors: [ThemeSettingsAccentColor]
let currentColor: ThemeSettingsColorOption?
let updated: (ThemeSettingsColorOption?) -> Void
let contextAction: ((Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void)?
let openColorPicker: (Bool) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, sectionId: ItemListSectionId, generalThemeReference: PresentationThemeReference, themeReference: PresentationThemeReference, colors: [ThemeSettingsAccentColor], currentColor: ThemeSettingsColorOption?, updated: @escaping (ThemeSettingsColorOption?) -> Void, contextAction: ((Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.generalThemeReference = generalThemeReference
self.themeReference = themeReference
self.colors = colors
self.currentColor = currentColor
self.updated = updated
self.contextAction = contextAction
self.openColorPicker = openColorPicker
self.tag = tag
self.sectionId = sectionId
}
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 = ThemeSettingsAccentColorItemNode()
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() })
})
}
}
}
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? ThemeSettingsAccentColorItemNode {
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 struct ThemeSettingsAccentColorItemNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let updatePosition: Bool
}
private func preparedTransition(action: @escaping (ThemeSettingsColorOption?, Bool) -> Void, contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)?, openColorPicker: @escaping (Bool) -> Void, from fromEntries: [ThemeSettingsColorEntry], to toEntries: [ThemeSettingsColorEntry], updatePosition: Bool) -> ThemeSettingsAccentColorItemNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action, contextAction: contextAction, openColorPicker: openColorPicker), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action, contextAction: contextAction, openColorPicker: openColorPicker), directionHint: nil) }
return ThemeSettingsAccentColorItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, updatePosition: updatePosition)
}
private func ensureColorVisible(listNode: ListView, accentColor: ThemeSettingsColorOption?, animated: Bool) -> Bool {
var resultNode: ThemeSettingsAccentColorIconItemNode?
listNode.forEachItemNode { node in
if resultNode == nil, let node = node as? ThemeSettingsAccentColorIconItemNode {
if node.item?.color?.index == accentColor?.index {
resultNode = node
}
}
}
if let resultNode = resultNode {
listNode.ensureItemNodeVisible(resultNode, animated: animated, overflow: 24.0)
return true
} else {
return false
}
}
class ThemeSettingsAccentColorItemNode: ListViewItemNode, ItemListItemNode {
private let containerNode: ASDisplayNode
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var snapshotView: UIView?
private let listNode: ListView
private var entries: [ThemeSettingsColorEntry]?
private var enqueuedTransitions: [ThemeSettingsAccentColorItemNodeTransition] = []
private var initialized = false
private var item: ThemeSettingsAccentColorItem?
private var layoutParams: ListViewItemLayoutParams?
private var tapping = false
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.containerNode = ASDisplayNode()
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.listNode = ListView()
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.containerNode)
self.addSubnode(self.listNode)
}
override func didLoad() {
super.didLoad()
self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true
}
private func enqueueTransition(_ transition: ThemeSettingsAccentColorItemNodeTransition) {
self.enqueuedTransitions.append(transition)
if let _ = self.item {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let item = self.item, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
let options = ListViewDeleteAndInsertOptions()
var scrollToItem: ListViewScrollToItem?
if !self.initialized || transition.updatePosition || !self.tapping {
if let index = item.colors.firstIndex(where: { $0.index == item.currentColor?.index }) {
scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-70.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down)
self.initialized = true
}
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
func asyncLayout() -> (_ item: ThemeSettingsAccentColorItem, _ 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: 60.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
if themeUpdated {
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.containerNode.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.containerNode.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.containerNode.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.containerNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height)
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))
var listInsets = UIEdgeInsets()
listInsets.top += params.leftInset + 4.0
listInsets.bottom += params.rightInset + 4.0
strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width)
strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
var entries: [ThemeSettingsColorEntry] = []
entries.append(.picker)
var index: Int = 0
for color in item.colors {
switch color {
case .default:
let selected = item.currentColor == nil
entries.append(.color(index, item.theme, item.generalThemeReference, nil, selected))
case let .color(color):
var selected = false
if let currentColor = item.currentColor, case let .accentColor(accentColor) = currentColor {
selected = accentColor.baseColor == color
}
let accentColor: ThemeSettingsColorOption
if let currentColor = item.currentColor, selected {
accentColor = currentColor
} else {
accentColor = .accentColor(PresentationThemeAccentColor(index: 10 + color.rawValue, baseColor: color))
}
switch accentColor {
case let .accentColor(color):
entries.append(.color(index, item.theme, item.generalThemeReference, color, selected))
case let .theme(theme):
entries.append(.theme(index, item.theme, item.generalThemeReference, theme, selected))
}
case let .preset(color), let .custom(color):
var selected = false
if let currentColor = item.currentColor {
selected = currentColor.index == Int64(color.index)
}
entries.append(.color(index, item.theme, item.themeReference, color, selected))
case let .theme(theme):
var selected = false
if let currentColor = item.currentColor {
selected = currentColor.index == theme.index
}
entries.append(.theme(index, item.theme, item.generalThemeReference, theme, selected))
}
index += 1
}
let action: (ThemeSettingsColorOption?, Bool) -> Void = { [weak self] color, selected in
if let strongSelf = self, let item = strongSelf.item {
if selected {
var create = true
if let color = color {
switch color {
case let .accentColor(color):
create = color.baseColor != .custom
case let .theme(theme):
if case let .cloud(theme) = theme {
create = !theme.theme.isCreator
}
}
}
item.openColorPicker(create)
} else {
strongSelf.tapping = true
item.updated(color)
Queue.mainQueue().after(0.4) {
strongSelf.tapping = false
}
}
let _ = ensureColorVisible(listNode: strongSelf.listNode, accentColor: color, animated: true)
}
}
let contextAction: ((ThemeSettingsColorOption?, Bool, ASDisplayNode, ContextGesture?) -> Void)? = { color, selected, node, gesture in
if let strongSelf = self, let item = strongSelf.item {
item.contextAction?(selected, item.generalThemeReference, color, node, gesture)
}
}
let openColorPicker: (Bool) -> Void = { [weak self] create in
if let strongSelf = self, let item = strongSelf.item {
item.openColorPicker(true)
}
}
let previousEntries = strongSelf.entries ?? []
let updatePosition = currentItem != nil && (previousEntries.count != entries.count || (currentItem?.generalThemeReference.index != item.generalThemeReference.index))
let transition = preparedTransition(action: action, contextAction: contextAction, openColorPicker: openColorPicker, from: previousEntries, to: entries, updatePosition: updatePosition)
strongSelf.enqueueTransition(transition)
strongSelf.entries = entries
}
})
}
}
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)
}
func prepareCrossfadeTransition() {
self.snapshotView?.removeFromSuperview()
if let snapshotView = self.containerNode.view.snapshotView(afterScreenUpdates: false) {
self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view)
self.snapshotView = snapshotView
}
}
func animateCrossfadeTransition() {
self.snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
self?.snapshotView?.removeFromSuperview()
})
}
}
@@ -0,0 +1,414 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AppBundle
private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(theme.list.itemBlocksBackgroundColor.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
context.fillEllipse(in: bounds)
context.setBlendMode(.normal)
let lineWidth: CGFloat
if selected {
var accentColor = theme.list.itemAccentColor
if accentColor.rgb == 0xffffff {
accentColor = UIColor(rgb: 0x999999)
}
context.setStrokeColor(accentColor.cgColor)
lineWidth = 2.0 - UIScreenPixel
} else {
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
lineWidth = 1.0 - UIScreenPixel
}
if bordered || selected {
context.setLineWidth(lineWidth)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
}
})?.stretchableImage(withLeftCapWidth: 15, topCapHeight: 15)
}
class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let theme: PresentationTheme
let strings: PresentationStrings
let systemStyle: ItemListSystemStyle
let icons: [PresentationAppIcon]
let isPremium: Bool
let currentIconName: String?
let updated: (PresentationAppIcon) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle, sectionId: ItemListSectionId, icons: [PresentationAppIcon], isPremium: Bool, currentIconName: String?, updated: @escaping (PresentationAppIcon) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.strings = strings
self.systemStyle = systemStyle
self.icons = icons
self.isPremium = isPremium
self.currentIconName = currentIconName
self.updated = updated
self.tag = tag
self.sectionId = sectionId
}
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 = ThemeSettingsAppIconItemNode()
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() })
})
}
}
}
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? ThemeSettingsAppIconItemNode {
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 let badgeSize = CGSize(width: 24.0, height: 24.0)
private let badgeStrokeSize: CGFloat = 2.0
private final class ThemeSettingsAppIconNode : ASDisplayNode {
private let iconNode: ASImageNode
private let overlayNode: ASImageNode
fileprivate let lockNode: ASImageNode
private let textNode: ImmediateTextNode
private var action: (() -> Void)?
private let activateAreaNode: AccessibilityAreaNode
private var locked = false
override init() {
self.iconNode = ASImageNode()
self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
self.iconNode.isLayerBacked = true
self.overlayNode = ASImageNode()
self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0))
self.overlayNode.isLayerBacked = true
self.lockNode = ASImageNode()
self.lockNode.contentMode = .scaleAspectFit
self.lockNode.displaysAsynchronously = false
self.lockNode.isUserInteractionEnabled = false
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.activateAreaNode = AccessibilityAreaNode()
self.activateAreaNode.accessibilityTraits = [.button]
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.overlayNode)
self.addSubnode(self.textNode)
self.addSubnode(self.lockNode)
self.addSubnode(self.activateAreaNode)
}
func setup(theme: PresentationTheme, icon: UIImage, title: NSAttributedString, locked: Bool, color: UIColor, bordered: Bool, selected: Bool, action: @escaping () -> Void) {
self.locked = locked
self.iconNode.image = icon
self.textNode.attributedText = title
self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected)
self.lockNode.isHidden = !locked
self.action = {
action()
}
self.activateAreaNode.accessibilityLabel = title.string
if locked {
self.activateAreaNode.accessibilityTraits = [.button, .notEnabled]
} else {
self.activateAreaNode.accessibilityTraits = [.button]
}
self.setNeedsLayout()
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.action?()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
let iconSize = CGSize(width: 63.0, height: 63.0)
self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 13.0), size: iconSize)
self.overlayNode.frame = self.iconNode.frame
let textSize = self.textNode.updateLayout(bounds.size)
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 81.0), size: textSize)
self.textNode.frame = textFrame
let badgeFinalSize = CGSize(width: badgeSize.width + badgeStrokeSize * 2.0, height: badgeSize.height + badgeStrokeSize * 2.0)
self.lockNode.frame = CGRect(x: bounds.width - 24.0, y: 4.0, width: badgeFinalSize.width, height: badgeFinalSize.height)
self.activateAreaNode.frame = bounds
}
}
private let textFont = Font.regular(12.0)
private let selectedTextFont = Font.medium(12.0)
class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var nodes: [ThemeSettingsAppIconNode] = []
private var item: ThemeSettingsAppIconItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
private var lockImage: UIImage?
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.containerNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.containerNode)
}
func asyncLayout() -> (_ item: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let nodeSize = CGSize(width: 74.0, height: 102.0)
let height: CGFloat = nodeSize.height * ceil(CGFloat(item.icons.count) / 4.0) + 12.0
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
let previousItem = strongSelf.item
strongSelf.item = item
strongSelf.layoutParams = params
if previousItem?.theme !== item.theme {
strongSelf.lockImage = generateImage(CGSize(width: badgeSize.width + badgeStrokeSize, height: badgeSize.height + badgeStrokeSize), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(item.theme.list.itemBlocksBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
context.addEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: badgeStrokeSize, dy: badgeStrokeSize))
context.clip()
var locations: [CGFloat] = [0.0, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
if let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: .white) {
context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false)
}
})
}
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, glass: item.systemStyle == .glass) : 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.containerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 2.0), size: CGSize(width: layoutSize.width - params.leftInset - params.rightInset, height: layoutSize.height))
let sideInset: CGFloat = 8.0
let spacing: CGFloat = floorToScreenPixels((params.width - sideInset * 2.0 - params.leftInset - params.rightInset - nodeSize.width * 4.0) / 3.0)
let verticalSpacing: CGFloat = 0.0
var x: CGFloat = sideInset
var y: CGFloat = 0.0
var i = 0
for icon in item.icons {
if i > 0 && i % 4 == 0 {
x = sideInset
y += nodeSize.height + verticalSpacing
}
let nodeFrame = CGRect(x: x, y: y, width: nodeSize.width, height: nodeSize.height)
x += nodeSize.width + spacing
let imageNode: ThemeSettingsAppIconNode
if strongSelf.nodes.count > i {
imageNode = strongSelf.nodes[i]
} else {
imageNode = ThemeSettingsAppIconNode()
strongSelf.nodes.append(imageNode)
strongSelf.containerNode.addSubnode(imageNode)
}
imageNode.lockNode.image = strongSelf.lockImage
if let image = UIImage(named: icon.imageName, in: getAppBundle(), compatibleWith: nil) {
let selected = icon.name == item.currentIconName
var name = "Icon"
var bordered = true
switch icon.name {
case "BlueIcon":
name = item.strings.Appearance_AppIconDefault
case "BlackIcon":
name = item.strings.Appearance_AppIconDefaultX
case "BlueClassicIcon":
name = item.strings.Appearance_AppIconClassic
case "BlackClassicIcon":
name = item.strings.Appearance_AppIconClassicX
case "BlueFilledIcon":
name = item.strings.Appearance_AppIconFilled
bordered = false
case "BlackFilledIcon":
name = item.strings.Appearance_AppIconFilledX
bordered = false
case "WhiteFilled":
name = " White"
case "New1":
name = item.strings.Appearance_AppIconNew1
case "New2":
name = item.strings.Appearance_AppIconNew2
case "Premium":
name = item.strings.Appearance_AppIconPremium
case "PremiumBlack":
name = item.strings.Appearance_AppIconBlack
case "PremiumTurbo":
name = item.strings.Appearance_AppIconTurbo
default:
name = icon.name
}
imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: {
item.updated(icon)
})
}
imageNode.frame = nodeFrame
i += 1
}
}
})
}
}
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)
}
}
@@ -0,0 +1,245 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import AppBundle
class ThemeSettingsBrightnessItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.value = value
self.sectionId = sectionId
self.updated = updated
}
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 = ThemeSettingsBrightnessItemNode()
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() })
})
}
}
}
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? ThemeSettingsBrightnessItemNode {
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 ThemeSettingsBrightnessItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let leftIconNode: ASImageNode
private let rightIconNode: ASImageNode
private var item: ThemeSettingsBrightnessItem?
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.leftIconNode = ASImageNode()
self.leftIconNode.displaysAsynchronously = false
self.leftIconNode.displayWithoutProcessing = true
self.rightIconNode = ASImageNode()
self.rightIconNode.displaysAsynchronously = false
self.rightIconNode.displayWithoutProcessing = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.leftIconNode)
self.addSubnode(self.rightIconNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.minimumValue = 0.0
sliderView.startValue = 0.0
sliderView.maximumValue = 100.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(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 + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.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: ThemeSettingsBrightnessItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedLeftIcon: UIImage?
var updatedRightIcon: UIImage?
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
updatedLeftIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMinIcon"), color: item.theme.list.itemPrimaryTextColor)
updatedRightIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsBrightnessMaxIcon"), color: item.theme.list.itemPrimaryTextColor)
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 60.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))
if let updatedLeftIcon = updatedLeftIcon {
strongSelf.leftIconNode.image = updatedLeftIcon
}
if let image = strongSelf.leftIconNode.image {
strongSelf.leftIconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height))
}
if let updatedRightIcon = updatedRightIcon {
strongSelf.rightIconNode.image = updatedRightIcon
}
if let image = strongSelf.rightIconNode.image {
strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height))
}
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 + 38.0, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 38.0 * 2.0, height: 44.0))
}
}
})
}
}
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(sliderView.value))
}
}
@@ -0,0 +1,313 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperBackgroundNode
struct ChatPreviewMessageItem: Equatable {
static func == (lhs: ChatPreviewMessageItem, rhs: ChatPreviewMessageItem) -> Bool {
if lhs.outgoing != rhs.outgoing {
return false
}
if let lhsReply = lhs.reply, let rhsReply = rhs.reply, lhsReply.0 != rhsReply.0 || lhsReply.1 != rhsReply.1 {
return false
} else if (lhs.reply == nil) != (rhs.reply == nil) {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.nameColor != rhs.nameColor {
return false
}
if lhs.backgroundEmojiId != rhs.backgroundEmojiId {
return false
}
return true
}
let outgoing: Bool
let reply: (String, String)?
let text: String
let nameColor: PeerColor
let backgroundEmojiId: Int64?
}
class ThemeSettingsChatPreviewItem: ListViewItem, ItemListItem {
let context: AccountContext
let systemStyle: ItemListSystemStyle
let theme: PresentationTheme
let componentTheme: PresentationTheme
let strings: PresentationStrings
let sectionId: ItemListSectionId
let fontSize: PresentationFontSize
let chatBubbleCorners: PresentationChatBubbleCorners
let wallpaper: TelegramWallpaper
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let messageItems: [ChatPreviewMessageItem]
init(context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [ChatPreviewMessageItem]) {
self.context = context
self.systemStyle = systemStyle
self.theme = theme
self.componentTheme = componentTheme
self.strings = strings
self.sectionId = sectionId
self.fontSize = fontSize
self.chatBubbleCorners = chatBubbleCorners
self.wallpaper = wallpaper
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.messageItems = messageItems
}
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 = ThemeSettingsChatPreviewItemNode()
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() })
})
}
}
}
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? ThemeSettingsChatPreviewItemNode {
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 ThemeSettingsChatPreviewItemNode: ListViewItemNode {
private var backgroundNode: WallpaperBackgroundNode?
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var messageNodes: [ListViewItemNode]?
private var item: ThemeSettingsChatPreviewItem?
private var finalImage = true
private let disposable = MetaDisposable()
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true
self.addSubnode(self.containerNode)
}
deinit {
self.disposable.dispose()
}
func asyncLayout() -> (_ item: ThemeSettingsChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentNodes = self.messageNodes
var currentBackgroundNode = self.backgroundNode
return { item, params, neighbors in
if currentBackgroundNode == nil {
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
}
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
let otherPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(2))
var items: [ListViewItem] = []
for messageItem in item.messageItems.reversed() {
var peers = SimpleDictionary<PeerId, Peer>()
var messages = SimpleDictionary<MessageId, Message>()
let replyMessageId = MessageId(peerId: peerId, namespace: 0, id: 3)
if let (author, text) = messageItem.reply {
peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: author, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: messageItem.nameColor, backgroundEmojiId: messageItem.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
messages[replyMessageId] = Message(stableId: 3, stableVersion: 0, id: replyMessageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[peerId], text: text, attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
}
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: messageItem.outgoing ? otherPeerId : peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: messageItem.outgoing ? [] : [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: messageItem.outgoing ? TelegramUser(id: otherPeerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil) : nil, text: messageItem.text, attributes: messageItem.reply != nil ? [ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil)] : [], media: [], peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
}
var nodes: [ListViewItemNode] = []
if let messageNodes = currentNodes {
nodes = messageNodes
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
apply(ListViewItemApply(isOnScreen: true))
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
}
nodes = messageNodes
}
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
for node in nodes {
contentSize.height += node.frame.size.height
}
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.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
strongSelf.messageNodes = nodes
var topOffset: CGFloat = 4.0
for node in nodes {
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize)
topOffset += node.frame.size.height
}
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
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 = 0.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.componentTheme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil
let backgroundFrame = 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)))
let displayMode: WallpaperDisplayMode
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
displayMode = .halfAspectFill
} else {
if backgroundFrame.width > backgroundFrame.height * 4.0 {
if params.availableHeight < 700.0 {
displayMode = .halfAspectFill
} else {
displayMode = .aspectFill
}
} else {
displayMode = .aspectFill
}
}
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = backgroundFrame.insetBy(dx: 0.0, dy: -100.0)
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
}
strongSelf.maskNode.frame = backgroundFrame.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))
}
})
}
}
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)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,348 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import AppBundle
class ThemeSettingsFontSizeItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let fontSize: PresentationFontSize
let disableLeadingInset: Bool
let displayIcons: Bool
let disableDecorations: Bool
let force: Bool
let enabled: Bool
let sectionId: ItemListSectionId
let updated: (PresentationFontSize) -> Void
let tag: ItemListItemTag?
init(theme: PresentationTheme, fontSize: PresentationFontSize, enabled: Bool = true, disableLeadingInset: Bool = false, displayIcons: Bool = true, disableDecorations: Bool = false, force: Bool = false, sectionId: ItemListSectionId, updated: @escaping (PresentationFontSize) -> Void, tag: ItemListItemTag? = nil) {
self.theme = theme
self.fontSize = fontSize
self.enabled = enabled
self.disableLeadingInset = disableLeadingInset
self.displayIcons = displayIcons
self.force = force
self.disableDecorations = disableDecorations
self.sectionId = sectionId
self.updated = updated
self.tag = tag
}
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 = ThemeSettingsFontSizeItemNode()
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() })
})
}
}
}
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? ThemeSettingsFontSizeItemNode {
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 ThemeSettingsFontSizeItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var sliderView: TGPhotoEditorSliderView?
private let leftIconNode: ASImageNode
private let rightIconNode: ASImageNode
private let disabledOverlayNode: ASDisplayNode
private var item: ThemeSettingsFontSizeItem?
private var layoutParams: ListViewItemLayoutParams?
var tag: ItemListItemTag? {
return self.item?.tag
}
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.leftIconNode = ASImageNode()
self.leftIconNode.displaysAsynchronously = false
self.leftIconNode.displayWithoutProcessing = true
self.rightIconNode = ASImageNode()
self.rightIconNode.displaysAsynchronously = false
self.rightIconNode.displayWithoutProcessing = true
self.disabledOverlayNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.leftIconNode)
self.addSubnode(self.rightIconNode)
self.addSubnode(self.disabledOverlayNode)
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 4.0
sliderView.dotSize = 8.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 6.0
sliderView.startValue = 0.0
sliderView.positionsCount = 7
sliderView.useLinesForPositions = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.isUserInteractionEnabled = item.enabled
let value: CGFloat
switch item.fontSize {
case .extraSmall:
value = 0.0
case .small:
value = 1.0
case .medium:
value = 2.0
case .regular:
value = 3.0
case .large:
value = 4.0
case .extraLarge:
value = 5.0
case .extraLargeX2:
value = 6.0
}
sliderView.value = value
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0))
}
self.view.insertSubview(sliderView, belowSubview: self.disabledOverlayNode.view)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: ThemeSettingsFontSizeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { item, params, neighbors in
var updatedLeftIcon: UIImage?
var updatedRightIcon: UIImage?
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
updatedLeftIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMinIcon"), color: item.theme.list.itemPrimaryTextColor)
updatedRightIcon = generateTintedImage(image: UIImage(bundleImageName: "Instant View/SettingsFontMaxIcon"), color: item.theme.list.itemPrimaryTextColor)
}
let contentSize: CGSize
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
contentSize = CGSize(width: params.width, height: 60.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
if item.disableLeadingInset {
insets.top = 0.0
insets.bottom = 0.0
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
let firstTime = strongSelf.item == nil || item.force
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
strongSelf.disabledOverlayNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4)
strongSelf.disabledOverlayNode.isHidden = item.enabled
strongSelf.disabledOverlayNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: 44.0))
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) && !item.disableDecorations
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))
if let updatedLeftIcon = updatedLeftIcon {
strongSelf.leftIconNode.image = updatedLeftIcon
}
if let image = strongSelf.leftIconNode.image {
strongSelf.leftIconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 25.0), size: CGSize(width: image.size.width, height: image.size.height))
}
if let updatedRightIcon = updatedRightIcon {
strongSelf.rightIconNode.image = updatedRightIcon
}
if let image = strongSelf.rightIconNode.image {
strongSelf.rightIconNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 14.0 - image.size.width, y: 21.0), size: CGSize(width: image.size.width, height: image.size.height))
}
strongSelf.leftIconNode.isHidden = !item.displayIcons
strongSelf.rightIconNode.isHidden = !item.displayIcons
if let sliderView = strongSelf.sliderView {
sliderView.isUserInteractionEnabled = item.enabled
sliderView.trackColor = item.enabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
let value: CGFloat
switch item.fontSize {
case .extraSmall:
value = 0.0
case .small:
value = 1.0
case .medium:
value = 2.0
case .regular:
value = 3.0
case .large:
value = 4.0
case .extraLarge:
value = 5.0
case .extraLargeX2:
value = 6.0
}
if firstTime {
sliderView.value = value
}
let sliderInset: CGFloat = item.displayIcons ? 38.0 : 16.0
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + sliderInset, y: 8.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - sliderInset * 2.0, height: 44.0))
}
}
})
}
}
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
}
let fontSize: PresentationFontSize
switch Int(sliderView.value) {
case 0:
fontSize = .extraSmall
case 1:
fontSize = .small
case 2:
fontSize = .medium
case 3:
fontSize = .regular
case 4:
fontSize = .large
case 5:
fontSize = .extraLarge
case 6:
fontSize = .extraLargeX2
default:
fontSize = .regular
}
self.item?.updated(fontSize)
}
}