-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
StoryPoster.swift
180 lines (159 loc) · 6.19 KB
/
StoryPoster.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
import Foundation
import UIKit
import WordPressKit
import AutomatticTracks
/// A type representing the Story block
struct Story: Codable {
let mediaFiles: [MediaFile]
}
/// The contents of a Story block
struct MediaFile: Codable {
let alt: String
let caption: String
let id: String
let link: String
let mime: String
let type: String
let url: String
init(alt: String,
caption: String,
id: String,
link: String,
mime: String,
type: String,
url: String) {
self.alt = alt
self.caption = caption
self.id = id
self.link = link
self.mime = mime
self.type = type
self.url = url
}
init(dictionary: [String: Any]) throws {
// We must handle both possible types because the Gutenberg `replaceBlock` method seems to be changing the type of this field.
let id: String
do {
id = try dictionary.value(key: CodingKeys.id.stringValue, type: NSNumber.self).stringValue
} catch {
id = try dictionary.value(key: CodingKeys.id.stringValue, type: String.self)
}
self.init(alt: try dictionary.value(key: CodingKeys.alt.stringValue, type: String.self),
caption: try dictionary.value(key: CodingKeys.caption.stringValue, type: String.self),
id: id,
link: try dictionary.value(key: CodingKeys.link.stringValue, type: String.self),
mime: try dictionary.value(key: CodingKeys.mime.stringValue, type: String.self),
type: try dictionary.value(key: CodingKeys.type.stringValue, type: String.self),
url: try dictionary.value(key: CodingKeys.url.stringValue, type: String.self))
}
static func file(from dictionary: [String: Any]) -> MediaFile? {
do {
return try self.init(dictionary: dictionary)
} catch let error {
DDLogWarn("MediaFile error: \(error)")
return nil
}
}
}
extension Dictionary where Key == String, Value == Any {
enum ValueError: Error, CustomDebugStringConvertible {
case missingKey(String)
case wrongType(String, Any)
var debugDescription: String {
switch self {
case Dictionary.ValueError.missingKey(let key):
return "Dictionary is missing key: \(key)"
case Dictionary.ValueError.wrongType(let key, let value):
return "Dictionary has wrong type for \(key): \(type(of: value))"
}
}
}
func value<T: Any>(key: String, type: T.Type) throws -> T {
let value = self[key]
if let castValue = value as? T {
return castValue
} else {
if let value = value {
throw ValueError.wrongType(key, value)
} else {
throw ValueError.missingKey(key)
}
}
}
}
class StoryPoster {
struct MediaItem {
let url: URL
let size: CGSize
let archive: URL?
let original: URL?
var mimeType: String {
return url.mimeType
}
}
let context: NSManagedObjectContext
private let oldMediaFiles: [MediaFile]?
init(context: NSManagedObjectContext, mediaFiles: [MediaFile]?) {
self.context = context
self.oldMediaFiles = mediaFiles
}
/// Uploads media to a post and updates the post contents upon completion.
/// - Parameters:
/// - mediaItems: The media items to upload.
/// - post: The post to add media items to.
/// - completion: Called on completion with the new post or an error.
/// - Returns: `(String, [Media])` A tuple containing the Block which was added to contain the media and the new uploading Media objects will be returned.
func add(mediaItems: [MediaItem], post: AbstractPost) throws -> (String, [Media]) {
let assets = mediaItems.map { item in
return item.url as ExportableAsset
}
// Uploades the media and notifies upong completion with the updated post.
let media = PostCoordinator.shared.add(assets: assets, to: post).compactMap { return $0 }
// Update set of `MediaItem`s with values from the new added uploading `Media`.
let mediaFiles: [MediaFile] = media.enumerated().map { (idx, media) -> MediaFile in
let item = mediaItems[idx]
return MediaFile(alt: media.alt ?? "",
caption: media.caption ?? "",
id: String(media.gutenbergUploadID),
link: media.remoteURL ?? "",
mime: item.mimeType,
type: String(item.mimeType.split(separator: "/").first ?? ""),
url: item.archive?.absoluteString ?? "")
}
let story = Story(mediaFiles: mediaFiles)
let encoder = JSONEncoder()
let json = String(data: try encoder.encode(story), encoding: .utf8)
let block = StoryBlock.wrap(json ?? "", includeFooter: true)
return (block, media)
}
static var filePath: URL? = {
do {
let media = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("KanvasMedia")
try FileManager.default.createDirectory(at: media, withIntermediateDirectories: true, attributes: nil)
return media
} catch let error {
assertionFailure("Failed to create media file path: \(error)")
return nil
}
}()
}
struct StoryBlock {
private static let openTag = "<!-- wp:jetpack/story"
private static let closeTag = "-->"
private static let footer = """
<div class="wp-story wp-block-jetpack-story"></div>
<!-- /wp:jetpack/story -->
"""
/// Wraps the JSON of a Story into a story block.
/// - Parameter json: The JSON string to wrap in a story block.
/// - Returns: The string containing the full Story block.
static func wrap(_ json: String, includeFooter: Bool) -> String {
let content = """
\(openTag)
\(json)
\(closeTag)
\(includeFooter ? footer : "")
"""
return content
}
}