Support GIF + Better copy / pasting in the UITextView

This commit is contained in:
Thomas Ricouard 2023-02-07 16:26:27 +01:00
parent 036c42408e
commit 3b5f2e823a
4 changed files with 131 additions and 42 deletions

View file

@ -8,6 +8,7 @@ struct StatusEditorMediaContainer: Identifiable {
let id = UUID().uuidString
let image: UIImage?
let movieTransferable: MovieFileTranseferable?
let gifTransferable: GifFileTranseferable?
let mediaAttachment: MediaAttachment?
let error: Error?
}

View file

@ -24,7 +24,7 @@ struct StatusEditorMediaView: View {
makeLazyImage(mediaAttachement: attachement)
} else if container.image != nil {
makeLocalImage(container: container)
} else if container.movieTransferable != nil {
} else if container.movieTransferable != nil || container.gifTransferable != nil {
makeVideoAttachement(container: container)
} else if let error = container.error as? ServerError {
makeErrorView(error: error)

View file

@ -13,20 +13,31 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
case image = "public.image"
case jpeg = "public.jpeg"
case png = "public.png"
case tiff = "public.tiff"
case video = "public.video"
case movie = "public.movie"
case mp4 = "public.mpeg-4"
case gif = "public.gif"
case gif2 = "com.compuserve.gif"
case quickTimeMovie = "com.apple.quicktime-movie"
static func types() -> [UTType] {
[.url, .text, .plainText, .image, .jpeg, .png, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
[.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie]
}
var isVideo: Bool {
switch self {
case .video, .movie, .mp4, .gif, .quickTimeMovie:
case .video, .movie, .mp4, .quickTimeMovie:
return true
default:
return false
}
}
var isGif: Bool {
switch self {
case .gif, .gif2:
return true
default:
return false
@ -37,13 +48,18 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
let result = try await item.loadItem(forTypeIdentifier: rawValue)
if isVideo, let transferable = await getVideoTransferable(item: item) {
return transferable
} else if isGif, let transferable = await getGifTransferable(item: item) {
return transferable
}
if self == .jpeg || self == .png,
let imageURL = result as? URL,
let data = try? Data(contentsOf: imageURL),
let image = UIImage(data: data)
{
return image
if self == .jpeg || self == .png || self == .tiff || self == .image {
if let imageURL = result as? URL,
let data = try? Data(contentsOf: imageURL),
let image = UIImage(data: data) {
return image
} else if let data = result as? Data,
let image = UIImage(data: data) {
return image
}
}
if let url = result as? URL {
return url.absoluteString
@ -68,6 +84,19 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
}
}
}
private func getGifTransferable(item: NSItemProvider) async -> GifFileTranseferable? {
return await withCheckedContinuation { continuation in
_ = item.loadTransferable(type: GifFileTranseferable.self) { result in
switch result {
case let .success(success):
continuation.resume(with: .success(success))
case .failure:
continuation.resume(with: .success(nil))
}
}
}
}
}
struct MovieFileTranseferable: Transferable {
@ -95,9 +124,7 @@ struct MovieFileTranseferable: Transferable {
FileRepresentation(contentType: .movie) { movie in
SentTransferredFile(movie.url)
} importing: { received in
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
try FileManager.default.copyItem(at: received.file, to: copy)
return Self(url: copy)
return Self(url: localURLFor(received: received))
}
}
}
@ -113,13 +140,33 @@ struct ImageFileTranseferable: Transferable {
FileRepresentation(contentType: .image) { image in
SentTransferredFile(image.url)
} importing: { received in
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
try FileManager.default.copyItem(at: received.file, to: copy)
return Self(url: copy)
return Self(url: localURLFor(received: received))
}
}
}
struct GifFileTranseferable: Transferable {
let url: URL
var data: Data? {
try? Data(contentsOf: url)
}
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .gif) { gif in
SentTransferredFile(gif.url)
} importing: { received in
return Self(url: localURLFor(received: received))
}
}
}
fileprivate func localURLFor(received: ReceivedTransferredFile) -> URL {
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
try? FileManager.default.copyItem(at: received.file, to: copy)
return copy
}
public extension URL {
func mimeType() -> String {
if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {

View file

@ -7,7 +7,7 @@ import PhotosUI
import SwiftUI
@MainActor
public class StatusEditorViewModel: ObservableObject {
public class StatusEditorViewModel: NSObject, ObservableObject {
var mode: Mode
var client: Client?
@ -15,7 +15,11 @@ public class StatusEditorViewModel: ObservableObject {
var theme: Theme?
var preferences: UserPreferences?
var textView: UITextView?
var textView: UITextView? {
didSet {
textView?.pasteDelegate = self
}
}
var selectedRange: NSRange {
get {
guard let textView else {
@ -261,6 +265,7 @@ public class StatusEditorViewModel: ObservableObject {
visibility = status.visibility
mediasImages = status.mediaAttachments.map { .init(image: nil,
movieTransferable: nil,
gifTransferable: nil,
mediaAttachment: $0,
error: nil) }
case let .quote(status):
@ -333,24 +338,11 @@ public class StatusEditorViewModel: ObservableObject {
urlLengthAdjustments = totalUrlLength - (maxLengthOfUrl * numUrls)
var mediaAdded = false
statusText.enumerateAttributes(in: range) { attributes, range, _ in
if let attachment = attributes[.attachment] as? NSTextAttachment, let image = attachment.image {
mediasImages.append(.init(image: image,
movieTransferable: nil,
mediaAttachment: nil,
error: nil))
statusText.removeAttribute(.attachment, range: range)
statusText.mutableString.deleteCharacters(in: range)
mediaAdded = true
} else if attributes[.link] != nil {
if attributes[.link] != nil {
statusText.removeAttribute(.link, range: range)
}
}
if mediaAdded {
processMediasToUpload()
}
} catch {}
}
@ -370,11 +362,19 @@ public class StatusEditorViewModel: ObservableObject {
} else if let image = content as? UIImage {
mediasImages.append(.init(image: image,
movieTransferable: nil,
gifTransferable: nil,
mediaAttachment: nil,
error: nil))
} else if let video = content as? MovieFileTranseferable {
mediasImages.append(.init(image: nil,
movieTransferable: video,
gifTransferable: nil,
mediaAttachment: nil,
error: nil))
} else if let gif = content as? GifFileTranseferable {
mediasImages.append(.init(image: nil,
movieTransferable: nil,
gifTransferable: gif,
mediaAttachment: nil,
error: nil))
}
@ -492,17 +492,17 @@ public class StatusEditorViewModel: ObservableObject {
Task {
var medias: [StatusEditorMediaContainer] = []
for media in selectedMedias {
print(media.supportedContentTypes)
var file: (any Transferable)?
do {
file = try await media.loadTransferable(type: ImageFileTranseferable.self)
if file == nil {
file = try await media.loadTransferable(type: MovieFileTranseferable.self)
}
} catch {
medias.append(.init(image: nil,
movieTransferable: nil,
mediaAttachment: nil,
error: error))
if file == nil {
file = try? await media.loadTransferable(type: GifFileTranseferable.self)
}
if file == nil {
file = try? await media.loadTransferable(type: MovieFileTranseferable.self)
}
if file == nil {
file = try? await media.loadTransferable(type: ImageFileTranseferable.self)
}
if var imageFile = file as? ImageFileTranseferable,
@ -510,11 +510,19 @@ public class StatusEditorViewModel: ObservableObject {
{
medias.append(.init(image: image,
movieTransferable: nil,
gifTransferable: nil,
mediaAttachment: nil,
error: nil))
} else if let videoFile = file as? MovieFileTranseferable {
medias.append(.init(image: nil,
movieTransferable: videoFile,
gifTransferable: nil,
mediaAttachment: nil,
error: nil))
} else if let gifFile = file as? GifFileTranseferable {
medias.append(.init(image: nil,
movieTransferable: nil,
gifTransferable: gifFile,
mediaAttachment: nil,
error: nil))
}
@ -543,8 +551,10 @@ public class StatusEditorViewModel: ObservableObject {
func upload(container: StatusEditorMediaContainer) async {
if let index = indexOf(container: container) {
let originalContainer = mediasImages[index]
guard originalContainer.mediaAttachment == nil else { return }
let newContainer = StatusEditorMediaContainer(image: originalContainer.image,
movieTransferable: originalContainer.movieTransferable,
gifTransferable: nil,
mediaAttachment: nil,
error: nil)
mediasImages[index] = newContainer
@ -556,6 +566,7 @@ public class StatusEditorViewModel: ObservableObject {
let uploadedMedia = try await uploadMedia(data: data, mimeType: "image/jpeg")
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
movieTransferable: nil,
gifTransferable: nil,
mediaAttachment: uploadedMedia,
error: nil)
if let uploadedMedia, uploadedMedia.url == nil {
@ -567,6 +578,17 @@ public class StatusEditorViewModel: ObservableObject {
let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType())
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
movieTransferable: originalContainer.movieTransferable,
gifTransferable: nil,
mediaAttachment: uploadedMedia,
error: nil)
if let uploadedMedia, uploadedMedia.url == nil {
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
}
} else if let gifData = originalContainer.gifTransferable?.data {
let uploadedMedia = try await uploadMedia(data: gifData, mimeType: "image/gif")
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
movieTransferable: nil,
gifTransferable: originalContainer.gifTransferable,
mediaAttachment: uploadedMedia,
error: nil)
if let uploadedMedia, uploadedMedia.url == nil {
@ -578,6 +600,7 @@ public class StatusEditorViewModel: ObservableObject {
if let index = indexOf(container: newContainer) {
mediasImages[index] = .init(image: originalContainer.image,
movieTransferable: nil,
gifTransferable: nil,
mediaAttachment: nil,
error: error)
}
@ -601,6 +624,7 @@ public class StatusEditorViewModel: ObservableObject {
let oldContainer = mediasImages[index]
mediasImages[index] = .init(image: oldContainer.image,
movieTransferable: oldContainer.movieTransferable,
gifTransferable: oldContainer.gifTransferable,
mediaAttachment: newAttachement,
error: nil)
}
@ -619,6 +643,7 @@ public class StatusEditorViewModel: ObservableObject {
description: description))
mediasImages[index] = .init(image: nil,
movieTransferable: nil,
gifTransferable: nil,
mediaAttachment: media,
error: nil)
} catch {}
@ -636,7 +661,6 @@ public class StatusEditorViewModel: ObservableObject {
}
// MARK: - Custom emojis
func fetchCustomEmojis() async {
guard let client else { return }
do {
@ -645,6 +669,7 @@ public class StatusEditorViewModel: ObservableObject {
}
}
//MARK: - DropDelegate
extension StatusEditorViewModel: DropDelegate {
public func performDrop(info: DropInfo) -> Bool {
let item = info.itemProviders(for: StatusEditorUTTypeSupported.types())
@ -652,3 +677,19 @@ extension StatusEditorViewModel: DropDelegate {
return true
}
}
// MARK: - UITextPasteDelegate
extension StatusEditorViewModel: UITextPasteDelegate {
public func textPasteConfigurationSupporting(
_ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting,
transform item: UITextPasteItem) {
if !item.itemProvider.registeredContentTypes(conformingTo: .image).isEmpty ||
!item.itemProvider.registeredContentTypes(conformingTo: .video).isEmpty ||
!item.itemProvider.registeredContentTypes(conformingTo: .gif).isEmpty {
processItemsProvider(items: [item.itemProvider])
item.setNoResult()
} else {
item.setDefaultResult()
}
}
}