Support GIF + Better copy / pasting in the UITextView
This commit is contained in:
parent
036c42408e
commit
3b5f2e823a
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue