import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import LegacyComponents import TelegramPresentationData import TelegramUIPreferences import OverlayStatusController import AccountContext import ShareController import SearchUI import HexColor import PresentationDataUtils import MediaPickerUI import WallpaperGalleryScreen public enum WallpaperSelectionResult { case remove case emoticon(String) case custom(wallpaperEntry: WallpaperGalleryEntry, options: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?) } public final class ThemeGridController: ViewController { public enum Mode { case generic case peer(EnginePeer, [TelegramTheme], TelegramWallpaper?, Int?, Int?) } private var controllerNode: ThemeGridControllerNode { return self.displayNode as! ThemeGridControllerNode } private let _ready = Promise() public override var ready: Promise { return self._ready } private let context: AccountContext private let mode: Mode private var presentationData: PresentationData private let presentationDataPromise = Promise() private var presentationDataDisposable: Disposable? private var searchContentNode: NavigationBarSearchContentNode? private var isEmpty: Bool? private var editingMode: Bool = false private var validLayout: ContainerViewLayout? public override var navigationBarRequiresEntireLayoutUpdate: Bool { return false } private var previousContentOffset: GridNodeVisibleContentOffset? public var completion: (WallpaperSelectionResult) -> Void = { _ in } public init(context: AccountContext, mode: Mode = .generic) { self.context = context self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationDataPromise.set(.single(self.presentationData)) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) switch mode { case .generic: self.title = self.presentationData.strings.Wallpaper_Title case .peer: self.title = self.presentationData.strings.Wallpaper_ChannelTitle } self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.scrollToTop = { [weak self] in if let strongSelf = self { if let searchContentNode = strongSelf.searchContentNode { searchContentNode.updateExpansionProgress(1.0, animated: true) } strongSelf.controllerNode.scrollToTop() } } self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings strongSelf.presentationData = presentationData strongSelf.presentationDataPromise.set(.single(presentationData)) if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.updateThemeAndStrings() } } }) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDataDisposable?.dispose() } private func updateThemeAndStrings() { self.title = self.presentationData.strings.Wallpaper_Title self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) if case .generic = self.mode { if let isEmpty = self.isEmpty, isEmpty { } else { if self.editingMode { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) } } } self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Wallpaper_Search) if self.isNodeLoaded { self.controllerNode.updatePresentationData(self.presentationData) } } public override func loadDisplayNode() { var mode: WallpaperGalleryController.Mode = .default var requiredLevel: Int? var requiredCustomLevel: Int? if case let .peer(peer, _, _, requiredLevelValue, requiredCustomLevelValue) = self.mode { mode = .peer(peer, false) requiredLevel = requiredLevelValue requiredCustomLevel = requiredCustomLevelValue } self.displayNode = ThemeGridControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, presentPreviewController: { [weak self] source in if let strongSelf = self { let dismissControllers = { [weak self] in if let self, let navigationController = self.navigationController as? NavigationController { var controllers = navigationController.viewControllers.filter({ controller in if controller is ThemeGridController { return false } return true }) navigationController.setViewControllers(controllers, animated: false) controllers = navigationController.viewControllers.filter({ controller in if controller is WallpaperGalleryController { return false } return true }) navigationController.setViewControllers(controllers, animated: true) } } let controller = WallpaperGalleryController(context: strongSelf.context, source: source, mode: mode) controller.requiredLevel = requiredLevel controller.apply = { [weak self, weak controller] wallpaper, options, editedImage, cropRect, brightness, _ in if let strongSelf = self { if case .peer = mode { var emoticon = "" if case let .wallpaper(wallpaper, _) = wallpaper { emoticon = wallpaper.settings?.emoticon ?? "" } strongSelf.completion(.emoticon(emoticon)) dismissControllers() } else { uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { [weak self, weak controller] in if let strongSelf = self { strongSelf.deactivateSearch(animated: false) strongSelf.controllerNode.scrollToTop(animated: false) } if let controller = controller { switch wallpaper { case .asset, .contextResult: controller.dismiss(animated: true) default: break } } }) } } } self?.push(controller) } }, presentGallery: { [weak self] in if let strongSelf = self { let dismissControllers = { [weak self] in if let self, let navigationController = self.navigationController as? NavigationController { if case .peer = mode { var controllers = navigationController.viewControllers.filter({ controller in if controller is ThemeGridController || controller is MediaPickerScreen { return false } return true }) navigationController.setViewControllers(controllers, animated: false) controllers = navigationController.viewControllers.filter({ controller in if controller is WallpaperGalleryController { return false } return true }) navigationController.setViewControllers(controllers, animated: true) } else { let controllers = navigationController.viewControllers.filter({ controller in if controller is WallpaperGalleryController || controller is MediaPickerScreen { return false } return true }) navigationController.setViewControllers(controllers, animated: true) } } } let controller = MediaPickerScreenImpl(context: strongSelf.context, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .wallpaper)) controller.customSelection = { [weak self] _, asset in guard let strongSelf = self, let asset = asset as? PHAsset else { return } let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: mode) controller.requiredLevel = requiredCustomLevel controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, _ in if let strongSelf = self { if case .peer = mode { strongSelf.completion(.custom(wallpaperEntry: wallpaper, options: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness)) Queue.mainQueue().after(0.15) { dismissControllers() } } else { uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { dismissControllers() }) } } } strongSelf.push(controller) } self?.push(controller) } }, presentColors: { [weak self] in if let strongSelf = self { let controller = ThemeColorsGridController(context: strongSelf.context) (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } }, emptyStateUpdated: { [weak self] empty in if let strongSelf = self { if empty != strongSelf.isEmpty { strongSelf.isEmpty = empty if case .generic = strongSelf.mode { if empty { strongSelf.navigationItem.setRightBarButton(nil, animated: true) } else { if strongSelf.editingMode { strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed)) } else { strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed)) } } } } } }, deleteWallpapers: { [weak self] wallpapers, completed in if let strongSelf = self { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Wallpaper_DeleteConfirmation(Int32(wallpapers.count)), color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() completed() guard let strongSelf = self else { return } for wallpaper in wallpapers { if wallpaper == strongSelf.presentationData.chatWallpaper { let presentationData = strongSelf.presentationData let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers let themeReference: PresentationThemeReference if presentationData.autoNightModeTriggered { themeReference = current.automaticThemeSwitchSetting.theme } else { themeReference = current.theme } themeSpecificChatWallpapers[themeReference.index] = nil themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: current.themeSpecificAccentColors[themeReference.index])] = nil return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers) })).start() break } } var deleteWallpapers: [Signal] = [] for wallpaper in wallpapers { deleteWallpapers.append(deleteWallpaper(account: strongSelf.context.account, wallpaper: wallpaper)) } let _ = (combineLatest(deleteWallpapers) |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self { strongSelf.controllerNode.updateWallpapers() } }) strongSelf.donePressed() })) actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) strongSelf.present(actionSheet, in: .window(.root)) } }, shareWallpapers: { [weak self] wallpapers in if let strongSelf = self { strongSelf.shareWallpapers(wallpapers) } }, resetWallpapers: { [weak self] in if let strongSelf = self { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) let items: [ActionSheetItem] = [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Wallpaper_ResetWallpapersConfirmation, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.scrollToTop?() let controller = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: nil)) strongSelf.present(controller, in: .window(.root)) let _ = resetWallpapers(account: strongSelf.context.account).start(completed: { [weak self, weak controller] in let _ = updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in return current.withUpdatedThemeSpecificChatWallpapers([:]) }).start() let _ = (strongSelf.context.sharedContext.accountManager.transaction { transaction in WallpapersState.update(transaction: transaction, { state in var state = state state.wallpapers.removeAll() return state }) }).start() let _ = (telegramWallpapers(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network) |> deliverOnMainQueue).start(completed: { [weak self, weak controller] in controller?.dismiss() if let strongSelf = self { strongSelf.controllerNode.updateWallpapers() } }) }) } }) ] actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) strongSelf.present(actionSheet, in: .window(.root)) } }, popViewController: { [weak self] in if let strongSelf = self { let _ = (strongSelf.navigationController as? NavigationController)?.popViewController(animated: true) } }) self.controllerNode.navigationBar = self.navigationBar self.controllerNode.requestDeactivateSearch = { [weak self] in self?.deactivateSearch(animated: true) } self.controllerNode.requestWallpaperRemoval = { [weak self] in if let self { self.completion(.remove) self.dismiss() } } self.controllerNode.gridNode.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { if let searchContentNode = strongSelf.searchContentNode { searchContentNode.updateGridVisibleContentOffset(offset) } var previousContentOffsetValue: CGFloat? if let previousContentOffset = strongSelf.previousContentOffset, case let .known(value) = previousContentOffset { previousContentOffsetValue = value } switch offset { case let .known(value): let transition: ContainedViewLayoutTransition if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 30.0 { transition = .animated(duration: 0.2, curve: .easeInOut) } else { transition = .immediate } strongSelf.navigationBar?.updateBackgroundAlpha(min(30.0, max(0.0, value - 54.0)) / 30.0, transition: transition) case .unknown, .none: strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) } strongSelf.previousContentOffset = offset } } self.controllerNode.gridNode.scrollingCompleted = { [weak self] in if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode { let _ = strongSelf.controllerNode.fixNavigationSearchableGridNodeScrolling(searchNode: searchContentNode) } } self._ready.set(self.controllerNode.ready.get()) self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) self.displayNodeDidLoad() } private func shareWallpapers(_ wallpapers: [TelegramWallpaper]) { var string: String = "" for wallpaper in wallpapers { var item: String? switch wallpaper { case let .file(file): var options: [String] = [] if file.isPattern { if file.settings.colors.count >= 1 { options.append("bg_color=\(UIColor(rgb: file.settings.colors[0]).hexString)") } if let intensity = file.settings.intensity { options.append("intensity=\(intensity)") } } var optionsString = "" if !options.isEmpty { optionsString = "?\(options.joined(separator: "&"))" } item = file.slug + optionsString case let .color(color): item = "\(UIColor(rgb: color).hexString)" default: break } if let item = item { if !string.isEmpty { string.append("\n") } string.append("https://t.me/bg/\(item)") } } let subject: ShareControllerSubject if wallpapers.count == 1 { subject = .url(string) } else { subject = .text(string) } let shareController = ShareController(context: context, subject: subject) self.present(shareController, in: .window(.root), blockInteraction: true) self.donePressed() } public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition) } func activateSearch() { if self.displayNavigationBar { let _ = (self.controllerNode.ready.get() |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return } if let scrollToTop = strongSelf.scrollToTop { scrollToTop() } if let searchContentNode = strongSelf.searchContentNode { strongSelf.controllerNode.activateSearch(placeholderNode: searchContentNode.placeholderNode) } strongSelf.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) }) } } func deactivateSearch(animated: Bool) { if !self.displayNavigationBar { self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) if let searchContentNode = self.searchContentNode { self.controllerNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated) } } } @objc func editPressed() { self.editingMode = true self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) self.searchContentNode?.setIsEnabled(false, animated: true) self.controllerNode.updateState { state in var state = state state.editing = true return state } } @objc func donePressed() { self.editingMode = false self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.searchContentNode?.setIsEnabled(true, animated: true) self.controllerNode.updateState { state in var state = state state.editing = false return state } } }