-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
GutenbergMediaInserterHelper.swift
272 lines (243 loc) · 11.8 KB
/
GutenbergMediaInserterHelper.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
import Foundation
import CoreServices
import Gutenberg
import MediaEditor
class GutenbergMediaInserterHelper: NSObject {
fileprivate let post: AbstractPost
fileprivate let gutenberg: Gutenberg
fileprivate let mediaCoordinator = MediaCoordinator.shared
fileprivate var mediaObserverReceipt: UUID?
/// Method of selecting media for upload, used for analytics
///
fileprivate var mediaSelectionMethod: MediaSelectionMethod = .none
var didPickMediaCallback: GutenbergMediaPickerHelperCallback?
init(post: AbstractPost, gutenberg: Gutenberg) {
self.post = post
self.gutenberg = gutenberg
super.init()
self.registerMediaObserver()
}
deinit {
self.unregisterMediaObserver()
}
func insertFromSiteMediaLibrary(media: [Media], callback: @escaping MediaPickerDidPickMediaCallback) {
let formattedMedia = media.map { item in
var metadata: [String: String] = [:]
if let videopressGUID = item.videopressGUID {
metadata["videopressGUID"] = videopressGUID
}
return MediaInfo(id: item.mediaID?.int32Value, url: item.remoteURL, type: item.mediaTypeString, caption: item.caption, title: item.filename, alt: item.alt, metadata: metadata)
}
callback(formattedMedia)
}
func insertFromDevice(_ selection: [Any], callback: @escaping MediaPickerDidPickMediaCallback) {
if let providers = selection as? [NSItemProvider] {
insertItemProviders(providers, callback: callback)
} else {
callback(nil)
}
}
private func insertItemProviders(_ providers: [NSItemProvider], callback: @escaping MediaPickerDidPickMediaCallback) {
let media: [MediaInfo] = providers.compactMap {
// WARNING: Media is a CoreData entity and has to be thread-confined
guard let media = insert(exportableAsset: $0, source: .deviceLibrary) else {
return nil
}
// Gutenberg fails to add an image if the preview `url` is `nil`. But
// it doesn't need to point anywhere. The placeholder gets displayed
// as soon as `MediaImportService` generated it (see `MediaState.thumbnailReady`).
// This way we, dramatically cut CPU and especially memory usage.
let previewURL = URL(fileURLWithPath: NSTemporaryDirectory() + "\(media.gutenbergUploadID)")
return MediaInfo(id: media.gutenbergUploadID, url: previewURL.absoluteString, type: media.mediaTypeString)
}
callback(media)
}
func insertFromDevice(url: URL, callback: @escaping MediaPickerDidPickMediaCallback, source: MediaSource = .otherApps) {
guard let media = insert(exportableAsset: url as NSURL, source: source) else {
callback([])
return
}
let mediaUploadID = media.gutenbergUploadID
callback([MediaInfo(id: mediaUploadID, url: url.absoluteString, type: media.mediaTypeString)])
}
func insertFromImage(image: UIImage, callback: @escaping MediaPickerDidPickMediaCallback, source: MediaSource = .deviceLibrary) {
guard let media = insert(exportableAsset: image, source: source) else {
callback([])
return
}
let mediaUploadID = media.gutenbergUploadID
let filePath = NSTemporaryDirectory() + "\(mediaUploadID).jpg"
let url = URL(fileURLWithPath: filePath)
do {
try image.writeJPEGToURL(url)
callback([MediaInfo(id: mediaUploadID, url: url.absoluteString, type: media.mediaTypeString)])
} catch {
callback([MediaInfo(id: mediaUploadID, url: nil, type: media.mediaTypeString)])
return
}
}
func insertFromMediaEditor(assets: [AsyncImage], callback: @escaping MediaPickerDidPickMediaCallback) {
var mediaCollection: [MediaInfo] = []
let group = DispatchGroup()
assets.forEach { asset in
group.enter()
if let image = asset.editedImage {
insertFromImage(image: image, callback: { media in
guard let media = media,
let selectedMedia = media.first else {
group.leave()
return
}
mediaCollection.append(selectedMedia)
group.leave()
})
}
}
group.notify(queue: .main) {
callback(mediaCollection)
}
}
func syncUploads() {
for media in post.media {
if media.remoteStatus == .failed {
gutenberg.mediaUploadUpdate(id: media.gutenbergUploadID, state: .uploading, progress: 0, url: media.absoluteThumbnailLocalURL, serverID: nil)
let finalState: Gutenberg.MediaUploadState = ReachabilityUtils.isInternetReachable() ? .failed : .paused
gutenberg.mediaUploadUpdate(id: media.gutenbergUploadID, state: finalState, progress: 0, url: nil, serverID: nil)
}
}
}
func mediaFor(uploadID: Int32) -> Media? {
for media in post.media {
if media.gutenbergUploadID == uploadID {
return media
}
}
return nil
}
func isUploadingMedia() -> Bool {
return mediaCoordinator.isUploadingMedia(for: post)
}
func cancelUploadOfAllMedia() {
mediaCoordinator.cancelUploadOfAllMedia(for: post)
}
func cancelUploadOf(media: Media) {
mediaCoordinator.cancelUploadAndDeleteMedia(media)
gutenberg.mediaUploadUpdate(id: media.gutenbergUploadID, state: .reset, progress: 0, url: nil, serverID: nil)
}
func retryUploadOf(media: Media) {
mediaCoordinator.retryMedia(media)
}
func retryFailedMediaUploads() {
_ = mediaCoordinator.uploadMedia(for: post, automatedRetry: true)
}
func hasFailedMedia() -> Bool {
return mediaCoordinator.hasFailedMedia(for: post)
}
func insert(exportableAsset: ExportableAsset, source: MediaSource) -> Media? {
let info = MediaAnalyticsInfo(origin: .editor(source), selectionMethod: mediaSelectionMethod)
return mediaCoordinator.addMedia(from: exportableAsset, to: self.post, analyticsInfo: info)
}
/// Method to be used to refresh the status of all media associated with the post.
/// this method should be called when opening a post to make sure every media block has the correct visual status.
func refreshMediaStatus() {
for media in post.media {
switch media.remoteStatus {
case .processing:
mediaObserver(media: media, state: .processing)
case .pushing:
var progressValue = 0.5
if let progress = mediaCoordinator.progress(for: media) {
progressValue = progress.fractionCompleted
}
mediaObserver(media: media, state: .progress(value: progressValue))
case .failed:
if let error = media.error as NSError? {
mediaObserver(media: media, state: .failed(error: error))
}
default:
break
}
}
}
private func registerMediaObserver() {
mediaObserverReceipt = mediaCoordinator.addObserver({ [weak self](media, state) in
self?.mediaObserver(media: media, state: state)
}, forMediaFor: post)
}
private func unregisterMediaObserver() {
if let receipt = mediaObserverReceipt {
mediaCoordinator.removeObserver(withUUID: receipt)
}
}
private func mediaObserver(media: Media, state: MediaCoordinator.MediaState) {
let mediaUploadID = media.gutenbergUploadID
switch state {
case .processing:
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .uploading, progress: 0, url: nil, serverID: nil)
case .thumbnailReady(let url) where ReachabilityUtils.isInternetReachable() && media.remoteStatus == .failed:
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .failed, progress: 0, url: url, serverID: nil)
case .thumbnailReady(let url) where !ReachabilityUtils.isInternetReachable() && media.remoteStatus == .failed:
// The progress value passed is ignored by the editor, allowing the UI to retain the last known progress before pausing
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .paused, progress: 0, url: url, serverID: nil)
case .thumbnailReady(let url):
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .uploading, progress: 0.20, url: url, serverID: nil)
break
case .uploading:
break
case .ended:
var currentURL = media.remoteURL
if media.remoteLargeURL != nil {
currentURL = media.remoteLargeURL
} else if media.remoteMediumURL != nil {
currentURL = media.remoteMediumURL
}
guard let urlString = currentURL, let url = URL(string: urlString), let mediaServerID = media.mediaID?.int32Value else {
break
}
switch media.mediaType {
case .video:
// Fetch metadata when is a VideoPress video
if media.videopressGUID != nil {
EditorMediaUtility.fetchVideoPressMetadata(for: media, in: post) { [weak self] (result) in
guard let strongSelf = self else {
return
}
switch result {
case .failure:
strongSelf.gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .failed, progress: 0, url: nil, serverID: nil)
case .success(let metadata):
strongSelf.gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .succeeded, progress: 1, url: metadata.originalURL, serverID: mediaServerID, metadata: metadata.asDictionary())
}
}
} else {
guard let remoteURLString = media.remoteURL, let remoteURL = URL(string: remoteURLString) else {
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .failed, progress: 0, url: nil, serverID: nil)
return
}
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .succeeded, progress: 1, url: remoteURL, serverID: mediaServerID)
}
default:
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .succeeded, progress: 1, url: url, serverID: mediaServerID)
}
case .failed(let error):
switch error.code {
case NSURLErrorCancelled:
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .reset, progress: 0, url: nil, serverID: nil)
case NSURLErrorNetworkConnectionLost: fallthrough
case NSURLErrorNotConnectedToInternet: fallthrough
case NSURLErrorTimedOut where !ReachabilityUtils.isInternetReachable():
// The progress value passed is ignored by the editor, allowing the UI to retain the last known progress before pausing
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .paused, progress: 0, url: nil, serverID: nil)
default:
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .failed, progress: 0, url: nil, serverID: nil)
}
case .progress(let value):
gutenberg.mediaUploadUpdate(id: mediaUploadID, state: .uploading, progress: Float(value), url: nil, serverID: nil)
}
}
}
extension Media {
var gutenbergUploadID: Int32 {
return Int32(truncatingIfNeeded: objectID.uriRepresentation().absoluteString.hash)
}
}