mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-06-08 02:53:56 +02:00
feat: add Ghostgram features
- Anti-Delete: save deleted messages locally - Ghost Mode: hide online status, read receipts - Voice Morpher: audio processing effects - Device Spoof: spoof device info - Custom GhostIcon app icon - User Notes: personal notes for contacts - Misc settings and controllers - GPLv2 License
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
// MARK: - Entry Definition
|
||||
|
||||
private enum DeletedMessagesSection: Int32 {
|
||||
case settings
|
||||
}
|
||||
|
||||
private enum DeletedMessagesEntry: ItemListNodeEntry {
|
||||
case enableToggle(PresentationTheme, String, Bool)
|
||||
case archiveMediaToggle(PresentationTheme, String, Bool)
|
||||
case settingsInfo(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
return DeletedMessagesSection.settings.rawValue
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .enableToggle:
|
||||
return 0
|
||||
case .archiveMediaToggle:
|
||||
return 1
|
||||
case .settingsInfo:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: DeletedMessagesEntry, rhs: DeletedMessagesEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .enableToggle(lhsTheme, lhsText, lhsValue):
|
||||
if case let .enableToggle(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .archiveMediaToggle(lhsTheme, lhsText, lhsValue):
|
||||
if case let .archiveMediaToggle(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .settingsInfo(lhsTheme, lhsText):
|
||||
if case let .settingsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: DeletedMessagesEntry, rhs: DeletedMessagesEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! DeletedMessagesControllerArguments
|
||||
switch self {
|
||||
case let .enableToggle(_, text, value):
|
||||
return ItemListSwitchItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
value: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
updated: { value in
|
||||
arguments.toggleEnabled(value)
|
||||
}
|
||||
)
|
||||
case let .archiveMediaToggle(_, text, value):
|
||||
return ItemListSwitchItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
value: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
updated: { value in
|
||||
arguments.toggleArchiveMedia(value)
|
||||
}
|
||||
)
|
||||
case let .settingsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
private final class DeletedMessagesControllerArguments {
|
||||
let toggleEnabled: (Bool) -> Void
|
||||
let toggleArchiveMedia: (Bool) -> Void
|
||||
|
||||
init(
|
||||
toggleEnabled: @escaping (Bool) -> Void,
|
||||
toggleArchiveMedia: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.toggleEnabled = toggleEnabled
|
||||
self.toggleArchiveMedia = toggleArchiveMedia
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private struct DeletedMessagesControllerState: Equatable {
|
||||
var isEnabled: Bool
|
||||
var archiveMedia: Bool
|
||||
|
||||
static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool {
|
||||
return lhs.isEnabled == rhs.isEnabled &&
|
||||
lhs.archiveMedia == rhs.archiveMedia
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entries builder
|
||||
|
||||
private func deletedMessagesControllerEntries(
|
||||
presentationData: PresentationData,
|
||||
state: DeletedMessagesControllerState
|
||||
) -> [DeletedMessagesEntry] {
|
||||
var entries: [DeletedMessagesEntry] = []
|
||||
|
||||
entries.append(.enableToggle(presentationData.theme, "Сохранять удалённые сообщения", state.isEnabled))
|
||||
entries.append(.archiveMediaToggle(presentationData.theme, "Архивировать медиа", state.archiveMedia))
|
||||
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Рядом со временем сообщения появится иконка корзины."))
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// MARK: - Controller
|
||||
|
||||
public func deletedMessagesController(context: AccountContext) -> ViewController {
|
||||
let initialState = DeletedMessagesControllerState(
|
||||
isEnabled: AntiDeleteManager.shared.isEnabled,
|
||||
archiveMedia: AntiDeleteManager.shared.archiveMedia
|
||||
)
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
let updateState: ((DeletedMessagesControllerState) -> DeletedMessagesControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
let arguments = DeletedMessagesControllerArguments(
|
||||
toggleEnabled: { value in
|
||||
AntiDeleteManager.shared.isEnabled = value
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.isEnabled = value
|
||||
return state
|
||||
}
|
||||
},
|
||||
toggleArchiveMedia: { value in
|
||||
AntiDeleteManager.shared.archiveMedia = value
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.archiveMedia = value
|
||||
return state
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let entries = deletedMessagesControllerEntries(presentationData: presentationData, state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Удалённые сообщения"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .blocks,
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
// MARK: - Entry Definition
|
||||
|
||||
private enum DeviceSpoofSection: Int32 {
|
||||
case enable
|
||||
case profiles
|
||||
case custom
|
||||
}
|
||||
|
||||
private enum DeviceSpoofEntry: ItemListNodeEntry {
|
||||
case enableHeader(PresentationTheme, String)
|
||||
case enableToggle(PresentationTheme, String, Bool)
|
||||
case enableInfo(PresentationTheme, String)
|
||||
case profilesHeader(PresentationTheme, String)
|
||||
case profile(PresentationTheme, Int, String, Bool)
|
||||
case customHeader(PresentationTheme, String)
|
||||
case customDeviceModel(PresentationTheme, String, String)
|
||||
case customSystemVersion(PresentationTheme, String, String)
|
||||
case customInfo(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .enableHeader, .enableToggle, .enableInfo:
|
||||
return DeviceSpoofSection.enable.rawValue
|
||||
case .profilesHeader, .profile:
|
||||
return DeviceSpoofSection.profiles.rawValue
|
||||
case .customHeader, .customDeviceModel, .customSystemVersion, .customInfo:
|
||||
return DeviceSpoofSection.custom.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .enableHeader: return 0
|
||||
case .enableToggle: return 1
|
||||
case .enableInfo: return 2
|
||||
case .profilesHeader: return 3
|
||||
case let .profile(_, id, _, _): return 10 + Int32(id)
|
||||
case .customHeader: return 500
|
||||
case .customDeviceModel: return 501
|
||||
case .customSystemVersion: return 502
|
||||
case .customInfo: return 503
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: DeviceSpoofEntry, rhs: DeviceSpoofEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .enableHeader(lhsTheme, lhsText):
|
||||
if case let .enableHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .enableToggle(lhsTheme, lhsText, lhsValue):
|
||||
if case let .enableToggle(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .enableInfo(lhsTheme, lhsText):
|
||||
if case let .enableInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .profilesHeader(lhsTheme, lhsText):
|
||||
if case let .profilesHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .profile(lhsTheme, lhsId, lhsName, lhsSelected):
|
||||
if case let .profile(rhsTheme, rhsId, rhsName, rhsSelected) = rhs,
|
||||
lhsTheme === rhsTheme, lhsId == rhsId, lhsName == rhsName, lhsSelected == rhsSelected {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .customHeader(lhsTheme, lhsText):
|
||||
if case let .customHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .customDeviceModel(lhsTheme, lhsTitle, lhsValue):
|
||||
if case let .customDeviceModel(rhsTheme, rhsTitle, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .customSystemVersion(lhsTheme, lhsTitle, lhsValue):
|
||||
if case let .customSystemVersion(rhsTheme, rhsTitle, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .customInfo(lhsTheme, lhsText):
|
||||
if case let .customInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: DeviceSpoofEntry, rhs: DeviceSpoofEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! DeviceSpoofControllerArguments
|
||||
switch self {
|
||||
case let .enableHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .enableToggle(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleEnabled(value)
|
||||
})
|
||||
case let .enableInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .profilesHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .profile(_, id, name, selected):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: name, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.selectProfile(id)
|
||||
})
|
||||
case let .customHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .customDeviceModel(_, title, value):
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: title), text: value, placeholder: "iPhone 14 Pro", sectionId: self.section, textUpdated: { text in
|
||||
arguments.updateCustomDeviceModel(text)
|
||||
}, action: {})
|
||||
case let .customSystemVersion(_, title, value):
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: title), text: value, placeholder: "iOS 17.2", sectionId: self.section, textUpdated: { text in
|
||||
arguments.updateCustomSystemVersion(text)
|
||||
}, action: {})
|
||||
case let .customInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
private final class DeviceSpoofControllerArguments {
|
||||
let toggleEnabled: (Bool) -> Void
|
||||
let selectProfile: (Int) -> Void
|
||||
let updateCustomDeviceModel: (String) -> Void
|
||||
let updateCustomSystemVersion: (String) -> Void
|
||||
|
||||
init(
|
||||
toggleEnabled: @escaping (Bool) -> Void,
|
||||
selectProfile: @escaping (Int) -> Void,
|
||||
updateCustomDeviceModel: @escaping (String) -> Void,
|
||||
updateCustomSystemVersion: @escaping (String) -> Void
|
||||
) {
|
||||
self.toggleEnabled = toggleEnabled
|
||||
self.selectProfile = selectProfile
|
||||
self.updateCustomDeviceModel = updateCustomDeviceModel
|
||||
self.updateCustomSystemVersion = updateCustomSystemVersion
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private struct DeviceSpoofControllerState: Equatable {
|
||||
var isEnabled: Bool
|
||||
var selectedProfileId: Int
|
||||
var customDeviceModel: String
|
||||
var customSystemVersion: String
|
||||
}
|
||||
|
||||
// MARK: - Entries Builder
|
||||
|
||||
private func deviceSpoofControllerEntries(presentationData: PresentationData, state: DeviceSpoofControllerState) -> [DeviceSpoofEntry] {
|
||||
var entries: [DeviceSpoofEntry] = []
|
||||
|
||||
let theme = presentationData.theme
|
||||
|
||||
entries.append(.enableHeader(theme, "ПОДМЕНА УСТРОЙСТВА"))
|
||||
entries.append(.enableToggle(theme, "Включить подмену", state.isEnabled))
|
||||
entries.append(.enableInfo(theme, "Изменяет информацию об устройстве для серверов Telegram. Требуется перезапуск приложения."))
|
||||
|
||||
entries.append(.profilesHeader(theme, "ВЫБЕРИТЕ УСТРОЙСТВО"))
|
||||
for profile in DeviceSpoofManager.profiles {
|
||||
let isSelected = profile.id == state.selectedProfileId
|
||||
entries.append(.profile(theme, profile.id, profile.name, isSelected))
|
||||
}
|
||||
|
||||
// Show custom input fields only when custom profile is selected
|
||||
if state.selectedProfileId == 100 {
|
||||
entries.append(.customHeader(theme, "СВОЁ УСТРОЙСТВО"))
|
||||
entries.append(.customDeviceModel(theme, "Модель: ", state.customDeviceModel))
|
||||
entries.append(.customSystemVersion(theme, "Система: ", state.customSystemVersion))
|
||||
|
||||
// Warning if fields are empty
|
||||
if state.customDeviceModel.isEmpty || state.customSystemVersion.isEmpty {
|
||||
entries.append(.customInfo(theme, "⚠️ Заполните оба поля. Пока поля пустые — используется реальное устройство."))
|
||||
} else {
|
||||
entries.append(.customInfo(theme, "Перезапустите приложение для применения."))
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// MARK: - Controller
|
||||
|
||||
public func deviceSpoofController(context: AccountContext) -> ViewController {
|
||||
let statePromise = ValuePromise(
|
||||
DeviceSpoofControllerState(
|
||||
isEnabled: DeviceSpoofManager.shared.isEnabled,
|
||||
selectedProfileId: DeviceSpoofManager.shared.selectedProfileId,
|
||||
customDeviceModel: DeviceSpoofManager.shared.customDeviceModel,
|
||||
customSystemVersion: DeviceSpoofManager.shared.customSystemVersion
|
||||
),
|
||||
ignoreRepeated: true
|
||||
)
|
||||
let stateValue = Atomic(value: DeviceSpoofControllerState(
|
||||
isEnabled: DeviceSpoofManager.shared.isEnabled,
|
||||
selectedProfileId: DeviceSpoofManager.shared.selectedProfileId,
|
||||
customDeviceModel: DeviceSpoofManager.shared.customDeviceModel,
|
||||
customSystemVersion: DeviceSpoofManager.shared.customSystemVersion
|
||||
))
|
||||
|
||||
let updateState: ((inout DeviceSpoofControllerState) -> Void) -> Void = { f in
|
||||
let result = stateValue.modify { state in
|
||||
var state = state
|
||||
f(&state)
|
||||
return state
|
||||
}
|
||||
statePromise.set(result)
|
||||
}
|
||||
|
||||
let arguments = DeviceSpoofControllerArguments(
|
||||
toggleEnabled: { value in
|
||||
DeviceSpoofManager.shared.isEnabled = value
|
||||
updateState { state in
|
||||
state.isEnabled = value
|
||||
}
|
||||
},
|
||||
selectProfile: { id in
|
||||
DeviceSpoofManager.shared.selectedProfileId = id
|
||||
updateState { state in
|
||||
state.selectedProfileId = id
|
||||
}
|
||||
},
|
||||
updateCustomDeviceModel: { text in
|
||||
DeviceSpoofManager.shared.customDeviceModel = text
|
||||
updateState { state in
|
||||
state.customDeviceModel = text
|
||||
}
|
||||
},
|
||||
updateCustomSystemVersion: { text in
|
||||
DeviceSpoofManager.shared.customSystemVersion = text
|
||||
updateState { state in
|
||||
state.customSystemVersion = text
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let entries = deviceSpoofControllerEntries(presentationData: presentationData, state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Подмена устройства"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .blocks,
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
// MARK: - Entry Definition
|
||||
|
||||
private enum GhostModeSection: Int32 {
|
||||
case master
|
||||
case features
|
||||
}
|
||||
|
||||
private enum GhostModeEntry: ItemListNodeEntry {
|
||||
case masterHeader(PresentationTheme, String)
|
||||
case masterToggle(PresentationTheme, String, Bool, Int, Int) // title, isOn, activeCount, totalCount
|
||||
case masterInfo(PresentationTheme, String)
|
||||
case featuresHeader(PresentationTheme, String)
|
||||
case hideReadReceipts(PresentationTheme, String, Bool)
|
||||
case hideStoryViews(PresentationTheme, String, Bool)
|
||||
case hideOnlineStatus(PresentationTheme, String, Bool)
|
||||
case hideTypingIndicator(PresentationTheme, String, Bool)
|
||||
case forceOffline(PresentationTheme, String, Bool)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .masterHeader, .masterToggle, .masterInfo:
|
||||
return GhostModeSection.master.rawValue
|
||||
case .featuresHeader, .hideReadReceipts, .hideStoryViews, .hideOnlineStatus, .hideTypingIndicator, .forceOffline:
|
||||
return GhostModeSection.features.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .masterHeader: return 0
|
||||
case .masterToggle: return 1
|
||||
case .masterInfo: return 2
|
||||
case .featuresHeader: return 3
|
||||
case .hideReadReceipts: return 4
|
||||
case .hideStoryViews: return 5
|
||||
case .hideOnlineStatus: return 6
|
||||
case .hideTypingIndicator: return 7
|
||||
case .forceOffline: return 8
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: GhostModeEntry, rhs: GhostModeEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .masterHeader(lhsTheme, lhsText):
|
||||
if case let .masterHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .masterToggle(lhsTheme, lhsText, lhsValue, lhsActive, lhsTotal):
|
||||
if case let .masterToggle(rhsTheme, rhsText, rhsValue, rhsActive, rhsTotal) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsActive == rhsActive, lhsTotal == rhsTotal {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .masterInfo(lhsTheme, lhsText):
|
||||
if case let .masterInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .featuresHeader(lhsTheme, lhsText):
|
||||
if case let .featuresHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .hideReadReceipts(lhsTheme, lhsText, lhsValue):
|
||||
if case let .hideReadReceipts(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .hideStoryViews(lhsTheme, lhsText, lhsValue):
|
||||
if case let .hideStoryViews(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .hideOnlineStatus(lhsTheme, lhsText, lhsValue):
|
||||
if case let .hideOnlineStatus(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .hideTypingIndicator(lhsTheme, lhsText, lhsValue):
|
||||
if case let .hideTypingIndicator(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .forceOffline(lhsTheme, lhsText, lhsValue):
|
||||
if case let .forceOffline(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: GhostModeEntry, rhs: GhostModeEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! GhostModeControllerArguments
|
||||
switch self {
|
||||
case let .masterHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .masterToggle(_, text, value, activeCount, totalCount):
|
||||
let title = "\(text) \(activeCount)/\(totalCount)"
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleMaster(value)
|
||||
})
|
||||
case let .masterInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .featuresHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .hideReadReceipts(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleHideReadReceipts()
|
||||
})
|
||||
case let .hideStoryViews(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleHideStoryViews()
|
||||
})
|
||||
case let .hideOnlineStatus(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleHideOnlineStatus()
|
||||
})
|
||||
case let .hideTypingIndicator(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleHideTypingIndicator()
|
||||
})
|
||||
case let .forceOffline(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleForceOffline()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
private final class GhostModeControllerArguments {
|
||||
let toggleMaster: (Bool) -> Void
|
||||
let toggleHideReadReceipts: () -> Void
|
||||
let toggleHideStoryViews: () -> Void
|
||||
let toggleHideOnlineStatus: () -> Void
|
||||
let toggleHideTypingIndicator: () -> Void
|
||||
let toggleForceOffline: () -> Void
|
||||
|
||||
init(
|
||||
toggleMaster: @escaping (Bool) -> Void,
|
||||
toggleHideReadReceipts: @escaping () -> Void,
|
||||
toggleHideStoryViews: @escaping () -> Void,
|
||||
toggleHideOnlineStatus: @escaping () -> Void,
|
||||
toggleHideTypingIndicator: @escaping () -> Void,
|
||||
toggleForceOffline: @escaping () -> Void
|
||||
) {
|
||||
self.toggleMaster = toggleMaster
|
||||
self.toggleHideReadReceipts = toggleHideReadReceipts
|
||||
self.toggleHideStoryViews = toggleHideStoryViews
|
||||
self.toggleHideOnlineStatus = toggleHideOnlineStatus
|
||||
self.toggleHideTypingIndicator = toggleHideTypingIndicator
|
||||
self.toggleForceOffline = toggleForceOffline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private struct GhostModeControllerState: Equatable {
|
||||
var isEnabled: Bool
|
||||
var hideReadReceipts: Bool
|
||||
var hideStoryViews: Bool
|
||||
var hideOnlineStatus: Bool
|
||||
var hideTypingIndicator: Bool
|
||||
var forceOffline: Bool
|
||||
|
||||
static func ==(lhs: GhostModeControllerState, rhs: GhostModeControllerState) -> Bool {
|
||||
return lhs.isEnabled == rhs.isEnabled &&
|
||||
lhs.hideReadReceipts == rhs.hideReadReceipts &&
|
||||
lhs.hideStoryViews == rhs.hideStoryViews &&
|
||||
lhs.hideOnlineStatus == rhs.hideOnlineStatus &&
|
||||
lhs.hideTypingIndicator == rhs.hideTypingIndicator &&
|
||||
lhs.forceOffline == rhs.forceOffline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entries Builder
|
||||
|
||||
private func ghostModeControllerEntries(presentationData: PresentationData, state: GhostModeControllerState) -> [GhostModeEntry] {
|
||||
var entries: [GhostModeEntry] = []
|
||||
|
||||
let theme = presentationData.theme
|
||||
|
||||
// Count active features
|
||||
var activeCount = 0
|
||||
if state.hideReadReceipts { activeCount += 1 }
|
||||
if state.hideStoryViews { activeCount += 1 }
|
||||
if state.hideOnlineStatus { activeCount += 1 }
|
||||
if state.hideTypingIndicator { activeCount += 1 }
|
||||
if state.forceOffline { activeCount += 1 }
|
||||
|
||||
// Master section
|
||||
entries.append(.masterHeader(theme, "РЕЖИМ ПРИЗРАКА"))
|
||||
entries.append(.masterToggle(theme, "Режим призрака", state.isEnabled, activeCount, 5))
|
||||
entries.append(.masterInfo(theme, "Когда включен, выбранные функции приватности будут активны."))
|
||||
|
||||
// Features section
|
||||
entries.append(.featuresHeader(theme, "ФУНКЦИИ"))
|
||||
entries.append(.hideReadReceipts(theme, "Не читать сообщения", state.hideReadReceipts))
|
||||
entries.append(.hideStoryViews(theme, "Не читать истории", state.hideStoryViews))
|
||||
entries.append(.hideOnlineStatus(theme, "Не отправлять «онлайн»", state.hideOnlineStatus))
|
||||
entries.append(.hideTypingIndicator(theme, "Не отправлять «печатает»", state.hideTypingIndicator))
|
||||
entries.append(.forceOffline(theme, "Автоматический «офлайн»", state.forceOffline))
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// MARK: - Controller
|
||||
|
||||
public func ghostModeController(context: AccountContext) -> ViewController {
|
||||
let statePromise = ValuePromise(
|
||||
GhostModeControllerState(
|
||||
isEnabled: GhostModeManager.shared.isEnabled,
|
||||
hideReadReceipts: GhostModeManager.shared.hideReadReceipts,
|
||||
hideStoryViews: GhostModeManager.shared.hideStoryViews,
|
||||
hideOnlineStatus: GhostModeManager.shared.hideOnlineStatus,
|
||||
hideTypingIndicator: GhostModeManager.shared.hideTypingIndicator,
|
||||
forceOffline: GhostModeManager.shared.forceOffline
|
||||
),
|
||||
ignoreRepeated: true
|
||||
)
|
||||
let stateValue = Atomic(value: GhostModeControllerState(
|
||||
isEnabled: GhostModeManager.shared.isEnabled,
|
||||
hideReadReceipts: GhostModeManager.shared.hideReadReceipts,
|
||||
hideStoryViews: GhostModeManager.shared.hideStoryViews,
|
||||
hideOnlineStatus: GhostModeManager.shared.hideOnlineStatus,
|
||||
hideTypingIndicator: GhostModeManager.shared.hideTypingIndicator,
|
||||
forceOffline: GhostModeManager.shared.forceOffline
|
||||
))
|
||||
|
||||
let updateState: ((inout GhostModeControllerState) -> Void) -> Void = { f in
|
||||
let result = stateValue.modify { state in
|
||||
var state = state
|
||||
f(&state)
|
||||
return state
|
||||
}
|
||||
statePromise.set(result)
|
||||
}
|
||||
|
||||
let arguments = GhostModeControllerArguments(
|
||||
toggleMaster: { value in
|
||||
GhostModeManager.shared.isEnabled = value
|
||||
updateState { state in
|
||||
state.isEnabled = value
|
||||
}
|
||||
},
|
||||
toggleHideReadReceipts: {
|
||||
let newValue = !GhostModeManager.shared.hideReadReceipts
|
||||
GhostModeManager.shared.hideReadReceipts = newValue
|
||||
updateState { state in
|
||||
state.hideReadReceipts = newValue
|
||||
}
|
||||
},
|
||||
toggleHideStoryViews: {
|
||||
let newValue = !GhostModeManager.shared.hideStoryViews
|
||||
GhostModeManager.shared.hideStoryViews = newValue
|
||||
updateState { state in
|
||||
state.hideStoryViews = newValue
|
||||
}
|
||||
},
|
||||
toggleHideOnlineStatus: {
|
||||
let newValue = !GhostModeManager.shared.hideOnlineStatus
|
||||
GhostModeManager.shared.hideOnlineStatus = newValue
|
||||
updateState { state in
|
||||
state.hideOnlineStatus = newValue
|
||||
}
|
||||
},
|
||||
toggleHideTypingIndicator: {
|
||||
let newValue = !GhostModeManager.shared.hideTypingIndicator
|
||||
GhostModeManager.shared.hideTypingIndicator = newValue
|
||||
updateState { state in
|
||||
state.hideTypingIndicator = newValue
|
||||
}
|
||||
},
|
||||
toggleForceOffline: {
|
||||
let newValue = !GhostModeManager.shared.forceOffline
|
||||
GhostModeManager.shared.forceOffline = newValue
|
||||
updateState { state in
|
||||
state.forceOffline = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let entries = ghostModeControllerEntries(presentationData: presentationData, state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Режим призрака"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .blocks,
|
||||
animateChanges: true
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
// MARK: - Entry Definition
|
||||
|
||||
private enum GhostgramSettingsSection: Int32 {
|
||||
case features
|
||||
}
|
||||
|
||||
private enum GhostgramSettingsEntry: ItemListNodeEntry {
|
||||
case deletedMessages(PresentationTheme, String, String)
|
||||
case ghostMode(PresentationTheme, String, String)
|
||||
case misc(PresentationTheme, String, String)
|
||||
case deviceSpoof(PresentationTheme, String, String)
|
||||
case voiceMorpher(PresentationTheme, String, String)
|
||||
case info(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
return GhostgramSettingsSection.features.rawValue
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .deletedMessages:
|
||||
return 0
|
||||
case .ghostMode:
|
||||
return 1
|
||||
case .misc:
|
||||
return 2
|
||||
case .deviceSpoof:
|
||||
return 3
|
||||
case .voiceMorpher:
|
||||
return 4
|
||||
case .info:
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: GhostgramSettingsEntry, rhs: GhostgramSettingsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .deletedMessages(lhsTheme, lhsText, lhsValue):
|
||||
if case let .deletedMessages(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .ghostMode(lhsTheme, lhsText, lhsValue):
|
||||
if case let .ghostMode(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .misc(lhsTheme, lhsText, lhsValue):
|
||||
if case let .misc(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .deviceSpoof(lhsTheme, lhsText, lhsValue):
|
||||
if case let .deviceSpoof(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .voiceMorpher(lhsTheme, lhsText, lhsValue):
|
||||
if case let .voiceMorpher(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .info(lhsTheme, lhsText):
|
||||
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: GhostgramSettingsEntry, rhs: GhostgramSettingsEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! GhostgramSettingsControllerArguments
|
||||
switch self {
|
||||
case let .deletedMessages(_, text, value):
|
||||
return ItemListDisclosureItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
label: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
action: {
|
||||
arguments.openDeletedMessages()
|
||||
}
|
||||
)
|
||||
case let .ghostMode(_, text, value):
|
||||
return ItemListDisclosureItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
label: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
action: {
|
||||
arguments.openGhostMode()
|
||||
}
|
||||
)
|
||||
case let .misc(_, text, value):
|
||||
return ItemListDisclosureItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
label: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
action: {
|
||||
arguments.openMisc()
|
||||
}
|
||||
)
|
||||
case let .deviceSpoof(_, text, value):
|
||||
return ItemListDisclosureItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
label: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
action: {
|
||||
arguments.openDeviceSpoof()
|
||||
}
|
||||
)
|
||||
case let .voiceMorpher(_, text, value):
|
||||
return ItemListDisclosureItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
label: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
action: {
|
||||
arguments.openVoiceMorpher()
|
||||
}
|
||||
)
|
||||
case let .info(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
private final class GhostgramSettingsControllerArguments {
|
||||
let openDeletedMessages: () -> Void
|
||||
let openGhostMode: () -> Void
|
||||
let openMisc: () -> Void
|
||||
let openDeviceSpoof: () -> Void
|
||||
let openVoiceMorpher: () -> Void
|
||||
|
||||
init(
|
||||
openDeletedMessages: @escaping () -> Void,
|
||||
openGhostMode: @escaping () -> Void,
|
||||
openMisc: @escaping () -> Void,
|
||||
openDeviceSpoof: @escaping () -> Void,
|
||||
openVoiceMorpher: @escaping () -> Void
|
||||
) {
|
||||
self.openDeletedMessages = openDeletedMessages
|
||||
self.openGhostMode = openGhostMode
|
||||
self.openMisc = openMisc
|
||||
self.openDeviceSpoof = openDeviceSpoof
|
||||
self.openVoiceMorpher = openVoiceMorpher
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private struct GhostgramSettingsState: Equatable {
|
||||
var deletedMessagesEnabled: Bool
|
||||
var ghostModeEnabled: Bool
|
||||
var ghostModeActiveCount: Int
|
||||
var miscEnabled: Bool
|
||||
var miscActiveCount: Int
|
||||
var deviceSpoofEnabled: Bool
|
||||
var voiceMorpherEnabled: Bool
|
||||
|
||||
static func current() -> GhostgramSettingsState {
|
||||
return GhostgramSettingsState(
|
||||
deletedMessagesEnabled: AntiDeleteManager.shared.isEnabled,
|
||||
ghostModeEnabled: GhostModeManager.shared.isEnabled,
|
||||
ghostModeActiveCount: GhostModeManager.shared.activeFeatureCount,
|
||||
miscEnabled: MiscSettingsManager.shared.isEnabled,
|
||||
miscActiveCount: MiscSettingsManager.shared.activeFeatureCount,
|
||||
deviceSpoofEnabled: DeviceSpoofManager.shared.isEnabled,
|
||||
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entries builder
|
||||
|
||||
private func ghostgramSettingsControllerEntries(
|
||||
presentationData: PresentationData,
|
||||
state: GhostgramSettingsState
|
||||
) -> [GhostgramSettingsEntry] {
|
||||
var entries: [GhostgramSettingsEntry] = []
|
||||
|
||||
// Deleted Messages
|
||||
let deletedStatus = state.deletedMessagesEnabled ? "Вкл" : "Выкл"
|
||||
entries.append(.deletedMessages(presentationData.theme, "Удалённые сообщения", deletedStatus))
|
||||
|
||||
// Ghost Mode
|
||||
let ghostModeStatus = state.ghostModeEnabled ? "\(state.ghostModeActiveCount)/5" : "Выкл"
|
||||
entries.append(.ghostMode(presentationData.theme, "Режим призрака", ghostModeStatus))
|
||||
|
||||
// Misc
|
||||
let miscStatus = state.miscEnabled ? "\(state.miscActiveCount)/5" : "Выкл"
|
||||
entries.append(.misc(presentationData.theme, "Прочее", miscStatus))
|
||||
|
||||
// Device Spoofing
|
||||
let deviceSpoofStatus = state.deviceSpoofEnabled ? "Вкл" : "Выкл"
|
||||
entries.append(.deviceSpoof(presentationData.theme, "Подмена устройства", deviceSpoofStatus))
|
||||
|
||||
// Voice Morpher
|
||||
let voiceMorpherStatus = state.voiceMorpherEnabled ? VoiceMorpherManager.shared.selectedPreset.name : "Выкл"
|
||||
entries.append(.voiceMorpher(presentationData.theme, "Голосовой двойник", voiceMorpherStatus))
|
||||
|
||||
// Info
|
||||
entries.append(.info(presentationData.theme, "Функции конфиденциальности Ghostgram. Скрытые отметки о прочтении, обход исчезающих сообщений, обход защиты от пересылки и другое."))
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// MARK: - Controller
|
||||
|
||||
public func ghostgramSettingsController(context: AccountContext) -> ViewController {
|
||||
var pushControllerImpl: ((ViewController, Bool) -> Void)?
|
||||
|
||||
let stateValue = Atomic(value: GhostgramSettingsState.current())
|
||||
let statePromise = ValuePromise(GhostgramSettingsState.current(), ignoreRepeated: true)
|
||||
|
||||
let arguments = GhostgramSettingsControllerArguments(
|
||||
openDeletedMessages: {
|
||||
pushControllerImpl?(deletedMessagesController(context: context), true)
|
||||
},
|
||||
openGhostMode: {
|
||||
pushControllerImpl?(ghostModeController(context: context), true)
|
||||
},
|
||||
openMisc: {
|
||||
pushControllerImpl?(miscController(context: context), true)
|
||||
},
|
||||
openDeviceSpoof: {
|
||||
pushControllerImpl?(deviceSpoofController(context: context), true)
|
||||
},
|
||||
openVoiceMorpher: {
|
||||
pushControllerImpl?(voiceMorpherController(context: context), true)
|
||||
}
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let entries = ghostgramSettingsControllerEntries(presentationData: presentationData, state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Ghostgram"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: true
|
||||
)
|
||||
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .blocks,
|
||||
animateChanges: true
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
||||
// Refresh state when view appears
|
||||
controller.visibleBottomContentOffsetChanged = { _ in }
|
||||
controller.didAppear = { _ in
|
||||
let newState = GhostgramSettingsState.current()
|
||||
let _ = stateValue.modify { _ in newState }
|
||||
statePromise.set(newState)
|
||||
}
|
||||
|
||||
pushControllerImpl = { [weak controller] c, animated in
|
||||
controller?.push(c)
|
||||
}
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
// MARK: - Entry Definition
|
||||
|
||||
private enum MiscSection: Int32 {
|
||||
case master
|
||||
case features
|
||||
}
|
||||
|
||||
private enum MiscEntry: ItemListNodeEntry {
|
||||
case masterHeader(PresentationTheme, String)
|
||||
case masterToggle(PresentationTheme, String, Bool, Int, Int)
|
||||
case masterInfo(PresentationTheme, String)
|
||||
case featuresHeader(PresentationTheme, String)
|
||||
case bypassCopyProtection(PresentationTheme, String, Bool)
|
||||
case disableViewOnceAutoDelete(PresentationTheme, String, Bool)
|
||||
case bypassScreenshotProtection(PresentationTheme, String, Bool)
|
||||
case blockAds(PresentationTheme, String, Bool)
|
||||
case alwaysOnline(PresentationTheme, String, Bool)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .masterHeader, .masterToggle, .masterInfo:
|
||||
return MiscSection.master.rawValue
|
||||
case .featuresHeader, .bypassCopyProtection, .disableViewOnceAutoDelete, .bypassScreenshotProtection, .blockAds, .alwaysOnline:
|
||||
return MiscSection.features.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .masterHeader: return 0
|
||||
case .masterToggle: return 1
|
||||
case .masterInfo: return 2
|
||||
case .featuresHeader: return 3
|
||||
case .bypassCopyProtection: return 4
|
||||
case .disableViewOnceAutoDelete: return 5
|
||||
case .bypassScreenshotProtection: return 6
|
||||
case .blockAds: return 7
|
||||
case .alwaysOnline: return 8
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: MiscEntry, rhs: MiscEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .masterHeader(lhsTheme, lhsText):
|
||||
if case let .masterHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .masterToggle(lhsTheme, lhsText, lhsValue, lhsActive, lhsTotal):
|
||||
if case let .masterToggle(rhsTheme, rhsText, rhsValue, rhsActive, rhsTotal) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsActive == rhsActive, lhsTotal == rhsTotal {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .masterInfo(lhsTheme, lhsText):
|
||||
if case let .masterInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .featuresHeader(lhsTheme, lhsText):
|
||||
if case let .featuresHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .bypassCopyProtection(lhsTheme, lhsText, lhsValue):
|
||||
if case let .bypassCopyProtection(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .disableViewOnceAutoDelete(lhsTheme, lhsText, lhsValue):
|
||||
if case let .disableViewOnceAutoDelete(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .bypassScreenshotProtection(lhsTheme, lhsText, lhsValue):
|
||||
if case let .bypassScreenshotProtection(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .blockAds(lhsTheme, lhsText, lhsValue):
|
||||
if case let .blockAds(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .alwaysOnline(lhsTheme, lhsText, lhsValue):
|
||||
if case let .alwaysOnline(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: MiscEntry, rhs: MiscEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! MiscControllerArguments
|
||||
switch self {
|
||||
case let .masterHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .masterToggle(_, text, value, activeCount, totalCount):
|
||||
let title = "\(text) \(activeCount)/\(totalCount)"
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleMaster(value)
|
||||
})
|
||||
case let .masterInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .featuresHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .bypassCopyProtection(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleBypassCopyProtection()
|
||||
})
|
||||
case let .disableViewOnceAutoDelete(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleDisableViewOnceAutoDelete()
|
||||
})
|
||||
case let .bypassScreenshotProtection(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleBypassScreenshotProtection()
|
||||
})
|
||||
case let .blockAds(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleBlockAds()
|
||||
})
|
||||
case let .alwaysOnline(_, text, value):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.toggleAlwaysOnline()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
private final class MiscControllerArguments {
|
||||
let toggleMaster: (Bool) -> Void
|
||||
let toggleBypassCopyProtection: () -> Void
|
||||
let toggleDisableViewOnceAutoDelete: () -> Void
|
||||
let toggleBypassScreenshotProtection: () -> Void
|
||||
let toggleBlockAds: () -> Void
|
||||
let toggleAlwaysOnline: () -> Void
|
||||
|
||||
init(
|
||||
toggleMaster: @escaping (Bool) -> Void,
|
||||
toggleBypassCopyProtection: @escaping () -> Void,
|
||||
toggleDisableViewOnceAutoDelete: @escaping () -> Void,
|
||||
toggleBypassScreenshotProtection: @escaping () -> Void,
|
||||
toggleBlockAds: @escaping () -> Void,
|
||||
toggleAlwaysOnline: @escaping () -> Void
|
||||
) {
|
||||
self.toggleMaster = toggleMaster
|
||||
self.toggleBypassCopyProtection = toggleBypassCopyProtection
|
||||
self.toggleDisableViewOnceAutoDelete = toggleDisableViewOnceAutoDelete
|
||||
self.toggleBypassScreenshotProtection = toggleBypassScreenshotProtection
|
||||
self.toggleBlockAds = toggleBlockAds
|
||||
self.toggleAlwaysOnline = toggleAlwaysOnline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private struct MiscControllerState: Equatable {
|
||||
var isEnabled: Bool
|
||||
var bypassCopyProtection: Bool
|
||||
var disableViewOnceAutoDelete: Bool
|
||||
var bypassScreenshotProtection: Bool
|
||||
var blockAds: Bool
|
||||
var alwaysOnline: Bool
|
||||
|
||||
static func ==(lhs: MiscControllerState, rhs: MiscControllerState) -> Bool {
|
||||
return lhs.isEnabled == rhs.isEnabled &&
|
||||
lhs.bypassCopyProtection == rhs.bypassCopyProtection &&
|
||||
lhs.disableViewOnceAutoDelete == rhs.disableViewOnceAutoDelete &&
|
||||
lhs.bypassScreenshotProtection == rhs.bypassScreenshotProtection &&
|
||||
lhs.blockAds == rhs.blockAds &&
|
||||
lhs.alwaysOnline == rhs.alwaysOnline
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entries Builder
|
||||
|
||||
private func miscControllerEntries(presentationData: PresentationData, state: MiscControllerState) -> [MiscEntry] {
|
||||
var entries: [MiscEntry] = []
|
||||
|
||||
let theme = presentationData.theme
|
||||
|
||||
var activeCount = 0
|
||||
if state.bypassCopyProtection { activeCount += 1 }
|
||||
if state.disableViewOnceAutoDelete { activeCount += 1 }
|
||||
if state.bypassScreenshotProtection { activeCount += 1 }
|
||||
if state.blockAds { activeCount += 1 }
|
||||
if state.alwaysOnline { activeCount += 1 }
|
||||
|
||||
entries.append(.masterHeader(theme, "РАСШИРЕННЫЕ ВОЗМОЖНОСТИ"))
|
||||
entries.append(.masterToggle(theme, "Misc", state.isEnabled, activeCount, 5))
|
||||
entries.append(.masterInfo(theme, "Когда включено, выбранные функции обхода ограничений будут активны."))
|
||||
|
||||
entries.append(.featuresHeader(theme, "ФУНКЦИИ"))
|
||||
entries.append(.bypassCopyProtection(theme, "Разрешить пересылку", state.bypassCopyProtection))
|
||||
entries.append(.disableViewOnceAutoDelete(theme, "Сохранять View Once", state.disableViewOnceAutoDelete))
|
||||
entries.append(.bypassScreenshotProtection(theme, "Разрешить скриншоты", state.bypassScreenshotProtection))
|
||||
entries.append(.blockAds(theme, "Блокировать рекламу", state.blockAds))
|
||||
entries.append(.alwaysOnline(theme, "Вечный онлайн", state.alwaysOnline))
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// MARK: - Controller
|
||||
|
||||
public func miscController(context: AccountContext) -> ViewController {
|
||||
let statePromise = ValuePromise(
|
||||
MiscControllerState(
|
||||
isEnabled: MiscSettingsManager.shared.isEnabled,
|
||||
bypassCopyProtection: MiscSettingsManager.shared.bypassCopyProtection,
|
||||
disableViewOnceAutoDelete: MiscSettingsManager.shared.disableViewOnceAutoDelete,
|
||||
bypassScreenshotProtection: MiscSettingsManager.shared.bypassScreenshotProtection,
|
||||
blockAds: MiscSettingsManager.shared.blockAds,
|
||||
alwaysOnline: MiscSettingsManager.shared.alwaysOnline
|
||||
),
|
||||
ignoreRepeated: true
|
||||
)
|
||||
let stateValue = Atomic(value: MiscControllerState(
|
||||
isEnabled: MiscSettingsManager.shared.isEnabled,
|
||||
bypassCopyProtection: MiscSettingsManager.shared.bypassCopyProtection,
|
||||
disableViewOnceAutoDelete: MiscSettingsManager.shared.disableViewOnceAutoDelete,
|
||||
bypassScreenshotProtection: MiscSettingsManager.shared.bypassScreenshotProtection,
|
||||
blockAds: MiscSettingsManager.shared.blockAds,
|
||||
alwaysOnline: MiscSettingsManager.shared.alwaysOnline
|
||||
))
|
||||
|
||||
let updateState: ((inout MiscControllerState) -> Void) -> Void = { f in
|
||||
let result = stateValue.modify { state in
|
||||
var state = state
|
||||
f(&state)
|
||||
return state
|
||||
}
|
||||
statePromise.set(result)
|
||||
}
|
||||
|
||||
let arguments = MiscControllerArguments(
|
||||
toggleMaster: { value in
|
||||
MiscSettingsManager.shared.isEnabled = value
|
||||
updateState { state in
|
||||
state.isEnabled = value
|
||||
}
|
||||
},
|
||||
toggleBypassCopyProtection: {
|
||||
let newValue = !MiscSettingsManager.shared.bypassCopyProtection
|
||||
MiscSettingsManager.shared.bypassCopyProtection = newValue
|
||||
updateState { state in
|
||||
state.bypassCopyProtection = newValue
|
||||
}
|
||||
},
|
||||
toggleDisableViewOnceAutoDelete: {
|
||||
let newValue = !MiscSettingsManager.shared.disableViewOnceAutoDelete
|
||||
MiscSettingsManager.shared.disableViewOnceAutoDelete = newValue
|
||||
updateState { state in
|
||||
state.disableViewOnceAutoDelete = newValue
|
||||
}
|
||||
},
|
||||
toggleBypassScreenshotProtection: {
|
||||
let newValue = !MiscSettingsManager.shared.bypassScreenshotProtection
|
||||
MiscSettingsManager.shared.bypassScreenshotProtection = newValue
|
||||
updateState { state in
|
||||
state.bypassScreenshotProtection = newValue
|
||||
}
|
||||
},
|
||||
toggleBlockAds: {
|
||||
let newValue = !MiscSettingsManager.shared.blockAds
|
||||
MiscSettingsManager.shared.blockAds = newValue
|
||||
updateState { state in
|
||||
state.blockAds = newValue
|
||||
}
|
||||
},
|
||||
toggleAlwaysOnline: {
|
||||
let newValue = !MiscSettingsManager.shared.alwaysOnline
|
||||
MiscSettingsManager.shared.alwaysOnline = newValue
|
||||
updateState { state in
|
||||
state.alwaysOnline = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let entries = miscControllerEntries(presentationData: presentationData, state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Misc"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .blocks,
|
||||
animateChanges: true
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
// MARK: - Entry Definition
|
||||
|
||||
private enum VoiceMorpherSection: Int32 {
|
||||
case enable
|
||||
case presets
|
||||
}
|
||||
|
||||
private enum VoiceMorpherEntry: ItemListNodeEntry {
|
||||
case enableHeader(PresentationTheme, String)
|
||||
case enableToggle(PresentationTheme, String, Bool)
|
||||
case enableInfo(PresentationTheme, String)
|
||||
case presetsHeader(PresentationTheme, String)
|
||||
case preset(PresentationTheme, Int, String, String, Bool) // id, name, description, selected
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .enableHeader, .enableToggle, .enableInfo:
|
||||
return VoiceMorpherSection.enable.rawValue
|
||||
case .presetsHeader, .preset:
|
||||
return VoiceMorpherSection.presets.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .enableHeader: return 0
|
||||
case .enableToggle: return 1
|
||||
case .enableInfo: return 2
|
||||
case .presetsHeader: return 3
|
||||
case let .preset(_, id, _, _, _): return 10 + Int32(id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: VoiceMorpherEntry, rhs: VoiceMorpherEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .enableHeader(lhsTheme, lhsText):
|
||||
if case let .enableHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .enableToggle(lhsTheme, lhsText, lhsValue):
|
||||
if case let .enableToggle(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .enableInfo(lhsTheme, lhsText):
|
||||
if case let .enableInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .presetsHeader(lhsTheme, lhsText):
|
||||
if case let .presetsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .preset(lhsTheme, lhsId, lhsName, lhsDesc, lhsSelected):
|
||||
if case let .preset(rhsTheme, rhsId, rhsName, rhsDesc, rhsSelected) = rhs,
|
||||
lhsTheme === rhsTheme, lhsId == rhsId, lhsName == rhsName, lhsDesc == rhsDesc, lhsSelected == rhsSelected {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: VoiceMorpherEntry, rhs: VoiceMorpherEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! VoiceMorpherControllerArguments
|
||||
switch self {
|
||||
case let .enableHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .enableToggle(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleEnabled(value)
|
||||
})
|
||||
case let .enableInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .presetsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .preset(_, id, name, _, selected):
|
||||
return ItemListCheckboxItem(presentationData: presentationData, title: name, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
||||
arguments.selectPreset(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
private final class VoiceMorpherControllerArguments {
|
||||
let toggleEnabled: (Bool) -> Void
|
||||
let selectPreset: (Int) -> Void
|
||||
|
||||
init(
|
||||
toggleEnabled: @escaping (Bool) -> Void,
|
||||
selectPreset: @escaping (Int) -> Void
|
||||
) {
|
||||
self.toggleEnabled = toggleEnabled
|
||||
self.selectPreset = selectPreset
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private struct VoiceMorpherControllerState: Equatable {
|
||||
var isEnabled: Bool
|
||||
var selectedPresetId: Int
|
||||
}
|
||||
|
||||
// MARK: - Entries Builder
|
||||
|
||||
private func voiceMorpherControllerEntries(presentationData: PresentationData, state: VoiceMorpherControllerState) -> [VoiceMorpherEntry] {
|
||||
var entries: [VoiceMorpherEntry] = []
|
||||
|
||||
let theme = presentationData.theme
|
||||
|
||||
entries.append(.enableHeader(theme, "ИЗМЕНЕНИЕ ГОЛОСА"))
|
||||
entries.append(.enableToggle(theme, "Включить Voice Morpher", state.isEnabled))
|
||||
entries.append(.enableInfo(theme, "Изменяет твой голос при записи голосовых сообщений. Использует встроенные аудио-эффекты iOS."))
|
||||
|
||||
entries.append(.presetsHeader(theme, "ВЫБЕРИТЕ ЭФФЕКТ"))
|
||||
|
||||
// Add all presets except disabled (it's controlled by toggle)
|
||||
for preset in VoiceMorpherManager.VoicePreset.allCases where preset != .disabled {
|
||||
let isSelected = preset.rawValue == state.selectedPresetId
|
||||
entries.append(.preset(theme, preset.rawValue, preset.name, preset.description, isSelected))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// MARK: - Controller
|
||||
|
||||
public func voiceMorpherController(context: AccountContext) -> ViewController {
|
||||
let statePromise = ValuePromise(
|
||||
VoiceMorpherControllerState(
|
||||
isEnabled: VoiceMorpherManager.shared.isEnabled,
|
||||
selectedPresetId: VoiceMorpherManager.shared.selectedPresetId == 0 ? 1 : VoiceMorpherManager.shared.selectedPresetId
|
||||
),
|
||||
ignoreRepeated: true
|
||||
)
|
||||
let stateValue = Atomic(value: VoiceMorpherControllerState(
|
||||
isEnabled: VoiceMorpherManager.shared.isEnabled,
|
||||
selectedPresetId: VoiceMorpherManager.shared.selectedPresetId == 0 ? 1 : VoiceMorpherManager.shared.selectedPresetId
|
||||
))
|
||||
|
||||
let updateState: ((inout VoiceMorpherControllerState) -> Void) -> Void = { f in
|
||||
let result = stateValue.modify { state in
|
||||
var state = state
|
||||
f(&state)
|
||||
return state
|
||||
}
|
||||
statePromise.set(result)
|
||||
}
|
||||
|
||||
let arguments = VoiceMorpherControllerArguments(
|
||||
toggleEnabled: { value in
|
||||
VoiceMorpherManager.shared.isEnabled = value
|
||||
updateState { state in
|
||||
state.isEnabled = value
|
||||
}
|
||||
},
|
||||
selectPreset: { id in
|
||||
VoiceMorpherManager.shared.selectedPresetId = id
|
||||
updateState { state in
|
||||
state.selectedPresetId = id
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let entries = voiceMorpherControllerEntries(presentationData: presentationData, state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Голосовой двойник"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .blocks,
|
||||
animateChanges: false
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
return controller
|
||||
}
|
||||
Reference in New Issue
Block a user