Files
ghostgram/submodules/SettingsUI/Sources/DeviceSpoofController.swift
T
ichmagmaus 812 03f35f40b5 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
2026-01-19 12:01:00 +01:00

293 lines
12 KiB
Swift

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
}