mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 22:27:13 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
194 lines
7.0 KiB
Swift
194 lines
7.0 KiB
Swift
import Foundation
|
|
import SwiftSignalKit
|
|
import GGCore
|
|
import GGFeaturesShared
|
|
|
|
/// Resolves what the user picked into a mask ready to render.
|
|
///
|
|
/// The frame path only reads (`mask(for:)`: one lock, one dictionary lookup). A mask that is not
|
|
/// ready starts its preparation once — built-in masks are immediate, own masks are read from disk
|
|
/// on a background queue, catalog masks are downloaded — and frames pass through untouched until it
|
|
/// is ready. A mask that failed is remembered and is not retried from the frame path: only picking
|
|
/// it again (or opening the masks screen) retries it, so an unavailable remote mask never touches
|
|
/// the disk or the network per frame.
|
|
final class GGMaskEngine {
|
|
static let shared = GGMaskEngine()
|
|
|
|
private enum State {
|
|
case preparing
|
|
case ready(GGPreparedMask)
|
|
case failed
|
|
}
|
|
|
|
private let lock = NSLock()
|
|
private var states: [GGMaskSelection: State] = [:]
|
|
private let queue = Queue(name: "org.ghostgram.masks.engine")
|
|
private var storeRevisionDisposable: Disposable?
|
|
|
|
/// Changes of what is selected or prepared (screens refresh their labels).
|
|
let changes = ValuePromise<Int>(0, ignoreRepeated: false)
|
|
private var changesValue = 0
|
|
|
|
init() {
|
|
// An edited own mask is re-read on its next frame.
|
|
self.storeRevisionDisposable = (GGMaskStore.shared.revision.get() |> deliverOn(self.queue)).start(next: { [weak self] _ in
|
|
self?.dropUserMasks()
|
|
})
|
|
}
|
|
|
|
// MARK: Selection
|
|
|
|
/// What a camera of this account shows: the anonymous mask while the mode is on, else the
|
|
/// user's own choice.
|
|
static func selection(for account: GGAccountID?) -> GGMaskSelection {
|
|
if let account, let override = GGAnonymousMode.maskOverride(for: account) {
|
|
let selection = GGMaskSelection(storage: override)
|
|
if case .catalog = selection, !GGMaskCatalog.isAvailable {
|
|
// A mask the catalog can not deliver would leave the face uncovered: the default
|
|
// anonymous mask instead.
|
|
return GGMaskSelection(storage: GGAnonymousMode.mask.defaultValue)
|
|
}
|
|
return selection
|
|
}
|
|
return GGMaskSelection(storage: GGMaskSettings.selection.value)
|
|
}
|
|
|
|
/// Whether the camera of this account needs the masks extension at all.
|
|
static func isActive(for account: GGAccountID?) -> Bool {
|
|
if GGMaskSettings.enabled.value {
|
|
return true
|
|
}
|
|
if let account, GGAnonymousMode.isActive.value(for: account) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// The user picked a mask by hand: it becomes the choice, the anonymous mode of this account
|
|
/// turns off (the manual choice wins), a failure of it is forgotten and it is prepared now.
|
|
func select(_ selection: GGMaskSelection, account: GGAccountID?) {
|
|
GGMaskSettings.selection.set(selection.storage)
|
|
if let account, GGAnonymousMode.isActive.value(for: account) {
|
|
GGAnonymousMode.isActive.set(false, for: account)
|
|
}
|
|
self.retry(selection)
|
|
}
|
|
|
|
/// Forgets a failure and prepares the mask again.
|
|
func retry(_ selection: GGMaskSelection) {
|
|
self.lock.lock()
|
|
if case .failed = self.states[selection] {
|
|
self.states[selection] = nil
|
|
}
|
|
self.lock.unlock()
|
|
let _ = self.mask(for: selection)
|
|
}
|
|
|
|
// MARK: Frame path
|
|
|
|
/// Any thread. nil = no mask, or it is still being prepared, or it failed.
|
|
func mask(for selection: GGMaskSelection) -> GGPreparedMask? {
|
|
if case .none = selection {
|
|
return nil
|
|
}
|
|
self.lock.lock()
|
|
let state = self.states[selection]
|
|
if state == nil {
|
|
self.states[selection] = .preparing
|
|
}
|
|
self.lock.unlock()
|
|
switch state {
|
|
case let .ready(mask):
|
|
return mask
|
|
case .preparing, .failed:
|
|
return nil
|
|
case .none:
|
|
self.prepare(selection)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// For labels and the studio: the prepared mask if it is already there.
|
|
func preparedMask(for selection: GGMaskSelection) -> GGPreparedMask? {
|
|
self.lock.lock()
|
|
defer {
|
|
self.lock.unlock()
|
|
}
|
|
if case let .ready(mask) = self.states[selection] {
|
|
return mask
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func prepare(_ selection: GGMaskSelection) {
|
|
switch selection {
|
|
case .none:
|
|
break
|
|
case let .builtIn(id):
|
|
if let manifest = GGBuiltInMasks.manifest(id: id) {
|
|
self.finish(selection, GGPreparedMask(selection: selection, manifest: manifest, assets: nil))
|
|
} else {
|
|
self.finish(selection, nil)
|
|
}
|
|
case let .user(id):
|
|
self.queue.async {
|
|
let store = GGMaskStore.shared
|
|
guard let manifest = store.loadManifest(id: id) else {
|
|
self.finish(selection, nil)
|
|
return
|
|
}
|
|
self.finish(selection, GGPreparedMask(selection: selection, manifest: manifest, assets: GGMaskAssets(directory: store.assetsDirectory(id: id))))
|
|
}
|
|
case let .catalog(id):
|
|
GGMaskCatalog.shared.prepare(id: id, completion: { [weak self] mask in
|
|
self?.finish(selection, mask)
|
|
})
|
|
}
|
|
}
|
|
|
|
private func finish(_ selection: GGMaskSelection, _ mask: GGPreparedMask?) {
|
|
self.lock.lock()
|
|
if let mask {
|
|
self.states[selection] = .ready(mask)
|
|
} else {
|
|
self.states[selection] = .failed
|
|
}
|
|
self.lock.unlock()
|
|
DispatchQueue.main.async {
|
|
self.changesValue += 1
|
|
self.changes.set(self.changesValue)
|
|
}
|
|
}
|
|
|
|
private func dropUserMasks() {
|
|
self.lock.lock()
|
|
for key in Array(self.states.keys) {
|
|
if case .user = key {
|
|
self.states[key] = nil
|
|
}
|
|
}
|
|
self.lock.unlock()
|
|
}
|
|
|
|
// MARK: Names
|
|
|
|
/// The name of a choice for labels («Не выбрана», a built-in name, a catalog or own mask name).
|
|
static func title(_ selection: GGMaskSelection) -> String {
|
|
let languageCode = GGLocalization.shared.languageCode
|
|
switch selection {
|
|
case .none:
|
|
return GGStrings.CameraMasks.Masks.none
|
|
case let .builtIn(id):
|
|
return GGBuiltInMasks.title(id: id)
|
|
case let .catalog(id):
|
|
if let entry = GGMaskCatalog.shared.entry(id: id) {
|
|
return entry.displayName(languageCode: languageCode)
|
|
}
|
|
// The catalog is off: the mask will never come (not a raw id in the menu).
|
|
return GGMaskCatalog.isAvailable ? id : GGStrings.CameraMasks.Masks.deletedMask
|
|
case let .user(id):
|
|
return GGMaskStore.shared.manifest(id: id)?.displayName(languageCode: languageCode) ?? GGStrings.CameraMasks.Masks.deletedMask
|
|
}
|
|
}
|
|
}
|