mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '9d2094fe23'
This commit is contained in:
commit
80d7198230
3 changed files with 153 additions and 35 deletions
|
|
@ -754,7 +754,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
|
||||
let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: media)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
|
|
|
|||
|
|
@ -3857,10 +3857,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
|
||||
if let (message, _, _) = strongSelf.contentInfo(), let image = message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil {
|
||||
let context = strongSelf.context
|
||||
var videoReference: AnyMediaReference?
|
||||
if let video = image.video {
|
||||
videoReference = .message(message: MessageReference(message), media: video)
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: image))
|
||||
let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: image), video: videoReference)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -3868,7 +3872,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
guard let controller = strongSelf.galleryController() else {
|
||||
return
|
||||
}
|
||||
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,50 +89,111 @@ public func fetchMediaData(context: AccountContext, postbox: Postbox, userLocati
|
|||
}
|
||||
}
|
||||
|
||||
public func saveToCameraRoll(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference) -> Signal<Float, NoError> {
|
||||
return fetchMediaData(context: context, postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: mediaReference)
|
||||
|> mapToSignal { state, isImage -> Signal<Float, NoError> in
|
||||
switch state {
|
||||
public func saveToCameraRoll(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference, video: AnyMediaReference? = nil) -> Signal<Float, NoError> {
|
||||
let mediaData: Signal<(FetchMediaDataState, Bool), NoError> = fetchMediaData(context: context, postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: mediaReference)
|
||||
let videoData: Signal<FetchMediaDataState?, NoError>
|
||||
if let video {
|
||||
videoData = fetchMediaData(context: context, postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: video)
|
||||
|> map { state, _ in
|
||||
return state
|
||||
}
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
videoData = .single(nil)
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
mediaData,
|
||||
videoData
|
||||
)
|
||||
|> mapToSignal { stateAndIsImage, videoStateAndIsImage -> Signal<Float, NoError> in
|
||||
let isImage = stateAndIsImage.1
|
||||
var mainData: MediaResourceData?
|
||||
var videoData: MediaResourceData?
|
||||
var waitForVideo = false
|
||||
if let videoState = videoStateAndIsImage {
|
||||
switch videoState {
|
||||
case let .progress(value):
|
||||
return .single(value * 0.95)
|
||||
case let .data(data):
|
||||
videoData = data
|
||||
}
|
||||
switch stateAndIsImage.0 {
|
||||
case let .progress(value):
|
||||
return .single(0.95 + 0.05 * value)
|
||||
case let .data(data):
|
||||
mainData = data
|
||||
}
|
||||
waitForVideo = true
|
||||
} else {
|
||||
switch stateAndIsImage.0 {
|
||||
case let .progress(value):
|
||||
return .single(value)
|
||||
case let .data(data):
|
||||
if data.complete {
|
||||
return Signal<Float, NoError> { subscriber in
|
||||
DeviceAccess.authorizeAccess(to: .mediaLibrary(.save), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: { c, a in
|
||||
context.sharedContext.presentGlobalController(c, a)
|
||||
}, openSettings: context.sharedContext.applicationBindings.openSettings, { authorized in
|
||||
if !authorized {
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4"
|
||||
mainData = data
|
||||
}
|
||||
}
|
||||
if let mainData, mainData.complete, videoData != nil || !waitForVideo {
|
||||
return Signal<Float, NoError> { subscriber in
|
||||
DeviceAccess.authorizeAccess(to: .mediaLibrary(.save), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: { c, a in
|
||||
context.sharedContext.presentGlobalController(c, a)
|
||||
}, openSettings: context.sharedContext.applicationBindings.openSettings, { authorized in
|
||||
if !authorized {
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4"
|
||||
if isImage, let videoData, let imageData = try? Data(contentsOf: URL(fileURLWithPath: mainData.path)) {
|
||||
let id = UUID().uuidString
|
||||
|
||||
let jpegWithID = addAssetIdentifierToJPEG(imageData, assetIdentifier: id)!
|
||||
let outputVideoURL = URL(fileURLWithPath: NSTemporaryDirectory() + "\(id).mov")
|
||||
|
||||
try? FileManager.default.copyItem(atPath: videoData.path, toPath: tempVideoPath)
|
||||
|
||||
addAssetIdentifierToVideo(inputURL: URL(fileURLWithPath: tempVideoPath), outputURL: outputVideoURL, assetIdentifier: id) { success in
|
||||
guard success else { return }
|
||||
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
if isImage {
|
||||
if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil)
|
||||
}
|
||||
} else {
|
||||
if let _ = try? FileManager.default.copyItem(atPath: data.path, toPath: tempVideoPath) {
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath))
|
||||
}
|
||||
}
|
||||
let request = PHAssetCreationRequest.forAsset()
|
||||
|
||||
request.addResource(with: .photo, data: jpegWithID, options: nil)
|
||||
request.addResource(with: .pairedVideo, fileURL: outputVideoURL, options: nil)
|
||||
}, completionHandler: { _, error in
|
||||
if let error = error {
|
||||
print("\(error)")
|
||||
}
|
||||
let _ = try? FileManager.default.removeItem(atPath: tempVideoPath)
|
||||
subscriber.putNext(1.0)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
}
|
||||
} else {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
if isImage {
|
||||
if let imageData = try? Data(contentsOf: URL(fileURLWithPath: mainData.path)) {
|
||||
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: imageData, options: nil)
|
||||
}
|
||||
} else {
|
||||
if let _ = try? FileManager.default.copyItem(atPath: mainData.path, toPath: tempVideoPath) {
|
||||
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath))
|
||||
}
|
||||
}
|
||||
}, completionHandler: { _, error in
|
||||
if let error {
|
||||
print("\(error)")
|
||||
}
|
||||
let _ = try? FileManager.default.removeItem(atPath: tempVideoPath)
|
||||
subscriber.putNext(1.0)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -160,3 +221,56 @@ public func copyToPasteboard(context: AccountContext, postbox: Postbox, userLoca
|
|||
}
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in return .complete() }
|
||||
}
|
||||
|
||||
private func addAssetIdentifierToJPEG(_ imageData: Data, assetIdentifier: String) -> Data? {
|
||||
guard let source = CGImageSourceCreateWithData(imageData as CFData, nil), let uti = CGImageSourceGetType(source), let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let mutableData = NSMutableData()
|
||||
guard let destination = CGImageDestinationCreateWithData(mutableData, uti, 1, nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] ?? [:]
|
||||
|
||||
var maker = metadata[kCGImagePropertyMakerAppleDictionary as String] as? [String: Any] ?? [:]
|
||||
maker["17"] = assetIdentifier
|
||||
metadata[kCGImagePropertyMakerAppleDictionary as String] = maker
|
||||
|
||||
CGImageDestinationAddImage(destination, cgImage, metadata as CFDictionary)
|
||||
CGImageDestinationFinalize(destination)
|
||||
|
||||
return mutableData as Data
|
||||
}
|
||||
|
||||
private func addAssetIdentifierToVideo(inputURL: URL, outputURL: URL, assetIdentifier: String, completion: @escaping (Bool) -> Void) {
|
||||
let asset = AVAsset(url: inputURL)
|
||||
|
||||
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
let identifierItem = AVMutableMetadataItem()
|
||||
identifierItem.keySpace = .quickTimeMetadata
|
||||
identifierItem.key = AVMetadataKey.quickTimeMetadataKeyContentIdentifier as NSString
|
||||
identifierItem.value = assetIdentifier as NSString
|
||||
|
||||
let stillImageTimeItem = AVMutableMetadataItem()
|
||||
let keyStillImageTime = "com.apple.quicktime.still-image-time"
|
||||
let keySpaceQuickTimeMetadata = "mdta"
|
||||
stillImageTimeItem.key = keyStillImageTime as (NSCopying & NSObjectProtocol)?
|
||||
stillImageTimeItem.keySpace = AVMetadataKeySpace(rawValue: keySpaceQuickTimeMetadata)
|
||||
stillImageTimeItem.value = 0 as (NSCopying & NSObjectProtocol)?
|
||||
stillImageTimeItem.dataType = "com.apple.metadata.datatype.int8"
|
||||
|
||||
exportSession.outputURL = outputURL
|
||||
exportSession.outputFileType = .mov
|
||||
exportSession.metadata = [identifierItem, stillImageTimeItem]
|
||||
exportSession.shouldOptimizeForNetworkUse = true
|
||||
|
||||
exportSession.exportAsynchronously {
|
||||
completion(exportSession.status == .completed)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue