Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

146 lines
6.6 KiB
Swift

import Foundation
import UIKit
import Photos
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import MediaEditor
import ComponentFlow
import AlertComponent
func mediaEditorCollageSubject(_ collage: MediaEditorCollage) -> MediaEditorScreenImpl.Subject? {
if !collage.isVideo {
guard let image = collage.image() else {
return nil
}
return .image(image: image, dimensions: PixelDimensions(collage.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false)
}
var items: [MediaEditorScreenImpl.Subject.VideoCollageItem] = []
for item in collage.items {
let content: MediaEditorScreenImpl.Subject.VideoCollageItem.Content
switch item.source {
case let .image(image, _):
content = .image(image)
case let .imageFile(path):
guard let image = mediaEditorCollageImage(path: path) else {
return nil
}
content = .image(image)
case let .videoFile(path):
content = .video(path, item.duration)
case let .videoAsset(asset):
content = .asset(asset)
}
items.append(MediaEditorScreenImpl.Subject.VideoCollageItem(content: content, frame: item.frame, contentScale: item.contentScale, contentOffset: item.contentOffset, id: item.id, isMain: item.id == collage.mainItemId))
}
return .videoCollage(items: items)
}
extension MediaEditorScreenImpl {
func resolveCollageDraft(_ draft: MediaEditorDraft) {
guard let manifest = draft.collage else {
return
}
let resolutionId = UUID()
self.collageResolutionId = resolutionId
let engine = self.context.engine
Queue.concurrentDefaultQueue().async { [weak self] in
let result = Swift.Result { try manifest.resolve(engine: engine) }
Queue.mainQueue().async {
guard let self, self.collageResolutionId == resolutionId else {
return
}
switch result {
case let .success(resolved):
if let collage = resolved.collage {
self.openCollage(collage, draft: draft)
} else {
self.node.readyForCollage()
self.presentCollageAlert(text: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Story_Editor_CollageDraftAllMediaMissing, completion: { [weak self] in
guard let self else {
return
}
if case let .draft(_, id) = self.node.actualSubject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
}
self.requestDismiss(saveDraft: false, animated: true, discardDraft: false)
})
}
case .failure:
self.node.readyForCollage()
self.presentCollageAlert(text: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Login_UnknownError, completion: { [weak self] in
self?.requestDismiss(saveDraft: false, animated: true, discardDraft: false)
})
}
}
}
}
func openCollage(_ collage: MediaEditorCollage, draft: MediaEditorDraft?, mediaPrepared: Bool = false) {
if draft != nil, collage.isVideo, !mediaPrepared {
self.node.readyForCollage()
self.prepareCollageMedia(collage) { [weak self] success in
guard let self else {
return
}
if success {
self.openCollage(collage, draft: draft, mediaPrepared: true)
} else {
self.requestDismiss(saveDraft: false, animated: true, discardDraft: false)
}
}
return
}
let resolutionId = self.collageResolutionId
Queue.concurrentDefaultQueue().async { [weak self] in
// Decoding and composing full-size photos must not block the picker transition.
let subject = mediaEditorCollageSubject(collage)
Queue.mainQueue().async {
guard let self, self.collageResolutionId == resolutionId else {
return
}
guard let subject else {
self.node.readyForCollage()
self.presentCollageAlert(text: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Login_UnknownError, completion: { [weak self] in
self?.requestDismiss(saveDraft: false, animated: true, discardDraft: false)
})
return
}
self.collage = collage
self.node.setup(subject: subject, privacy: draft?.privacy, values: draft.map { collage.valuesForEditor($0.values, resetVideoTimeline: $0.collage?.mainItemId != collage.mainItemId) }, caption: draft?.caption, isDraft: draft != nil)
}
}
}
func presentCollageAlert(text: String, completion: @escaping () -> Void = {}) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
Queue.mainQueue().justDispatch { [weak self] in
guard let self else {
return
}
weak var weakAlert: MediaEditorDraftSaveAlert?
var finished = false
let close = {
guard !finished else {
return
}
finished = true
weakAlert?.close(completion: completion)
}
let alert = MediaEditorDraftSaveAlert(
configuration: AlertScreen.Configuration(dismissOnOutsideTap: false),
contentSignal: .single([AnyComponentWithIdentity(id: "text", component: AnyComponent(AlertTextComponent(content: .plain(text))))]),
actionsSignal: .single([AlertScreen.Action(title: presentationData.strings.Common_OK, type: .default, action: close, autoDismiss: false)]),
updatedPresentationData: (initial: presentationData, signal: self.context.sharedContext.presentationData |> map { $0.withUpdated(theme: defaultDarkPresentationTheme) })
)
weakAlert = alert
alert.requestClose = {
close()
return false
}
self.present(alert, in: .window(.root))
}
}
}