ETLane 0.1.42 → 0.1.46

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ public extension Array where Element: Equatable {
2
+
3
+ /// Remove Dublicates
4
+ var unique: [Element] {
5
+ // Thanks to https://github.com/sairamkotha for improving the method
6
+ return self.reduce([]) { $0.contains($1) ? $0 : $0 + [$1] }
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ import Foundation
2
+
3
+ public extension Error {
4
+
5
+ var locd: String {
6
+ return "\(self.localizedDescription) - \(self)"
7
+ }
8
+
9
+ }
10
+
@@ -0,0 +1,34 @@
1
+ import CommonCrypto
2
+ import Foundation
3
+
4
+ public extension String {
5
+
6
+ var MD5: Data {
7
+ let messageData = self.data(using:.utf8)!
8
+ var digestData = Data(count: Int(CC_MD5_DIGEST_LENGTH))
9
+ _ = digestData.withUnsafeMutableBytes { digestBytes in
10
+ messageData.withUnsafeBytes { messageBytes in
11
+ CC_MD5(messageBytes.baseAddress, CC_LONG(messageData.count), digestBytes.bindMemory(to: UInt8.self).baseAddress)
12
+ }
13
+ }
14
+ return digestData
15
+ }
16
+
17
+ var SHA1: Data {
18
+ let data = Data(self.utf8)
19
+ var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH))
20
+ data.withUnsafeBytes {
21
+ _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
22
+ }
23
+ return data
24
+ }
25
+
26
+ var MD5String: String {
27
+ return self.MD5.map { String(format: "%02hhx", $0) }.joined()
28
+ }
29
+
30
+ var SHA1String: String {
31
+ return self.SHA1.map { String(format: "%02hhx", $0) }.joined()
32
+ }
33
+
34
+ }
@@ -0,0 +1,43 @@
1
+ import Common
2
+
3
+ extension Api {
4
+
5
+ func pages(
6
+ token: String,
7
+ projectId: String,
8
+ page: String
9
+ ) throws -> Figma.Pages {
10
+ try self.get(
11
+ path: "files/\(projectId)/nodes",
12
+ query: [
13
+ "ids" : page,
14
+ "depth": "1",
15
+ ],
16
+ headers: [
17
+ "X-FIGMA-TOKEN" : token
18
+ ],
19
+ timeoutInterval: 300
20
+ )
21
+ }
22
+
23
+ func images(
24
+ token: String,
25
+ projectId: String,
26
+ ids: [String],
27
+ scale: Int
28
+ ) throws -> Images {
29
+ try self.get(
30
+ path: "images/\(projectId)",
31
+ query: [
32
+ "ids" : ids.joined(separator: ","),
33
+ "format": "jpg",
34
+ "scale": "\(scale)",
35
+ ],
36
+ headers: [
37
+ "X-FIGMA-TOKEN" : token
38
+ ],
39
+ timeoutInterval: 300
40
+ )
41
+ }
42
+
43
+ }
@@ -0,0 +1,133 @@
1
+ import Foundation
2
+
3
+ struct Deploy {
4
+ private let keyValue: [Deploy.NamedKey: String]
5
+ }
6
+
7
+ extension Deploy.NamedKey {
8
+
9
+ var fileName: String? {
10
+ switch self {
11
+ case .title: return "name.txt"
12
+ case .subtitle: return "subtitle.txt"
13
+ case .keywords: return "keywords.txt"
14
+ case .whatsNew: return "release_notes.txt"
15
+ default: return nil
16
+ }
17
+ }
18
+
19
+ }
20
+
21
+ public extension Array {
22
+
23
+ func isIndexValid(index: Int) -> Bool {
24
+ return index >= 0 && index < self.count
25
+ }
26
+
27
+ func safeObject(at index: Int) -> Element? {
28
+ guard self.isIndexValid(index: index) else { return nil }
29
+ return self[index]
30
+ }
31
+ }
32
+
33
+ extension String {
34
+ func fixedValue() -> String {
35
+ self
36
+ .replacingOccurrences(of: "\\n", with: "\n")
37
+ .replacingOccurrences(of: "\r", with: "")
38
+ }
39
+ }
40
+
41
+ extension Deploy {
42
+
43
+ enum NamedKey: String, CaseIterable {
44
+ case title = "Title"
45
+ case subtitle = "Subtitle"
46
+ case keywords = "keywords"
47
+ case iPhone8 = "iPhone8"
48
+ case iPhone11 = "iPhone11"
49
+ case whatsNew = "What's new"
50
+ case locale = "locale"
51
+ case previewTimestamp
52
+ case iPadPro = "iPadPro"
53
+ case iPadPro3Gen = "iPadPro3Gen"
54
+ }
55
+
56
+ init(string: String, map: [Int: NamedKey]) {
57
+ let cmp = string.components(separatedBy: "\t")
58
+ var keyValue = [Deploy.NamedKey: String]()
59
+ cmp.enumerated().forEach { (idx, item) in
60
+ if let key = map[idx] {
61
+ keyValue[key] = item.fixedValue()
62
+ }
63
+ }
64
+ self.keyValue = keyValue
65
+ }
66
+
67
+ subscript(key: NamedKey) -> String {
68
+ let text = self.keyValue[key] ?? ""
69
+ return text
70
+ }
71
+
72
+ func createFiles(at url: URL) {
73
+ NamedKey.allCases.forEach {
74
+ if let fileName = $0.fileName {
75
+ url.write(self[$0], to: fileName)
76
+ }
77
+ }
78
+ }
79
+
80
+ }
81
+
82
+
83
+ extension URL {
84
+
85
+ func write(_ text: String, to path: String) {
86
+ let url = self.appendingPathComponent(path)
87
+ do {
88
+ print("Write \(url.path)")
89
+ try text.write(to: url, atomically: true, encoding: .utf8)
90
+ print("Done")
91
+ } catch {
92
+ print(">>>>>\(text) write error: \(error) to path \(url)")
93
+ }
94
+
95
+ }
96
+
97
+ }
98
+
99
+ extension Deploy {
100
+
101
+ static func fromTSV(_ url: String) throws -> [Deploy] {
102
+ let data = try Data(contentsOf: URL(string: url)!)
103
+ var map = [Int: Deploy.NamedKey]()
104
+ let deploys: [Deploy]
105
+ do {
106
+ let tsv = String(data: data, encoding: .utf8)!.components(separatedBy: "\n")
107
+ guard tsv.count > 1 else { print("TSV should have more than 1 line"); exit(-1) }
108
+ let keys = tsv[0].components(separatedBy: "\t")
109
+ print("Raw keys: \(keys)")
110
+ keys.enumerated().forEach { (idx, key) in
111
+ map[idx] = Deploy.NamedKey(rawValue: key.fixedValue())
112
+ }
113
+ print("Found keys: \(map.map({ "\($0.key):\($0.value.rawValue)" }))")
114
+ deploys = tsv.dropFirst().map { Deploy(string: $0, map: map) }
115
+ }
116
+ return deploys
117
+ }
118
+
119
+ }
120
+
121
+ //fileprivate extension String {
122
+ //
123
+ // func ids(scale: Int) -> [Deploy.IdWithScale] {
124
+ // return self.components(separatedBy: ",").map {
125
+ // ($0 as NSString).trimmingCharacters(in: CharacterSet(charactersIn: "0123456789:").inverted)
126
+ // }.filter {
127
+ // !$0.isEmpty
128
+ // }.map {
129
+ // Deploy.IdWithScale(id: $0, scale: scale)
130
+ // }
131
+ // }
132
+ //
133
+ //}
@@ -0,0 +1,42 @@
1
+ enum Device: String {
2
+ case iPhone8
3
+ case iPhone11
4
+ case iPhone8Messages = "iPhone8-message"
5
+ case iPhone11Messages = "iPhone11-message"
6
+ case iPadPro
7
+ case iPadPro3Gen
8
+ case iPadProMessages = "iPadPro-message"
9
+ case iPadPro3GenMessages = "iPadPro3Gen-message"
10
+ case watch = "Watch"
11
+ case watch4 = "Watch Series4"
12
+ }
13
+
14
+ extension Device {
15
+ var scale: Int {
16
+ switch self {
17
+ case .iPhone8, .iPhone11, .iPhone8Messages, .iPhone11Messages: return 3
18
+ case .iPadPro, .iPadPro3Gen, .iPadProMessages, .iPadPro3GenMessages, .watch, .watch4: return 2
19
+ }
20
+ }
21
+ var isIMessage: Bool {
22
+ switch self {
23
+ case .iPadProMessages, .iPadPro3GenMessages, .iPhone8Messages, .iPhone11Messages: return true
24
+ default: return false
25
+ }
26
+ }
27
+ /// ipadPro129 это обязательный компонент имени для iPad 3 Gen, все остальное определяется по размерам
28
+ var id: String {
29
+ switch self {
30
+ case .iPhone8: return "APP_IPHONE_55"
31
+ case .iPhone11: return "APP_IPHONE_65"
32
+ case .iPadPro: return "ipad-pro"
33
+ case .iPadPro3Gen: return "ipadPro129"
34
+ case .iPadProMessages: return "ipad-pro"
35
+ case .iPadPro3GenMessages: return "ipadPro129"
36
+ case .iPhone8Messages: return "APP_IPHONE_55"
37
+ case .iPhone11Messages: return "APP_IPHONE_65"
38
+ case .watch: return "APP_WATCH"
39
+ case .watch4: return "APP_WATCH_SERIES_4"
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,108 @@
1
+ import Foundation
2
+
3
+ class DownloadBatch {
4
+
5
+ static let kMaximumDownloadsCount = 3
6
+
7
+ private let images: [String: String]
8
+ private var imagesLeft = [String: String]()
9
+ private let downloadGroup = DispatchGroup()
10
+ private let session = URLSession.shared
11
+ private var imageData = [String: Data]()
12
+ private var currentDownloadKeys = Set<String>()
13
+ private let url: URL
14
+ private let syncQueue = DispatchQueue(label: "download_image_q")
15
+ private var isFinished = false
16
+
17
+ init(images: [String: String], url: URL) {
18
+ self.images = images
19
+ self.imagesLeft = images
20
+ self.url = url
21
+ }
22
+
23
+ func download() -> [Figma.PageId: Data] {
24
+ self.downloadGroup.enter()
25
+ self.downloadNext()
26
+ self.downloadGroup.wait()
27
+ return self.imageData
28
+ }
29
+
30
+ private func downloadNext() {
31
+ let isFinished = self.syncQueue.sync {
32
+ self.imagesLeft.isEmpty && self.currentDownloadKeys.isEmpty && !self.isFinished
33
+ }
34
+ let canDonwloadMore = self.syncQueue.sync {
35
+ self.currentDownloadKeys.count < DownloadBatch.kMaximumDownloadsCount
36
+ }
37
+ if isFinished {
38
+ self.isFinished = true
39
+ print("Download batch finished: \(self.images)")
40
+ self.downloadGroup.leave()
41
+ } else if canDonwloadMore {
42
+
43
+ if let first = self.imagesLeft.first {
44
+
45
+ self.syncQueue.sync {
46
+ self.imagesLeft.removeValue(forKey: first.key)
47
+ self.currentDownloadKeys.insert(first.key)
48
+ }
49
+ self.downloadItem(key: first.key, value: first.value, retryCount: 5) { data in
50
+ self.syncQueue.sync {
51
+ self.imageData[first.key] = data
52
+ _ = self.currentDownloadKeys.remove(first.key)
53
+ }
54
+ self.downloadNext()
55
+ }
56
+ self.downloadNext()
57
+ }
58
+ }
59
+ }
60
+
61
+ private func downloadItem(key: String, value: String, retryCount: Int, completion: @escaping (Data?) -> Void) {
62
+ let data = self.syncQueue.sync {
63
+ self.imageData[key]
64
+ }
65
+ if data != nil {
66
+ completion(data); return
67
+ }
68
+ if retryCount < 0 {
69
+ print("⛔️ Download image \(value) retry count limit")
70
+ completion(nil); return
71
+ }
72
+
73
+ let fileUrl = self.url.appendingPathComponent(value.cacheName)
74
+
75
+ if let data = try? Data(contentsOf: fileUrl) {
76
+ print("✅ Image already exist at \(value.cacheName), skip download \(value)")
77
+ completion(data)
78
+ return
79
+ }
80
+
81
+ let imageURL = URL(string: value)!
82
+ print("⬇️ Download image(\(retryCount)) with url: \(value)")
83
+ let request = URLRequest(
84
+ url: imageURL,
85
+ cachePolicy: .reloadIgnoringLocalCacheData,
86
+ timeoutInterval: 7 * 60
87
+ )
88
+ self.session.downloadTask(with: request) { (url, r, e) in
89
+ if let url = url {
90
+ do {
91
+ let data = try Data(contentsOf: url)
92
+ try data.write(to: fileUrl)
93
+ print("✅ Did finish \(value) at \(value.cacheName)")
94
+ completion(data)
95
+ } catch {
96
+ print("⛔️ Did fail download, retry: \(value), \(error)")
97
+ self.downloadItem(key: key, value: value, retryCount: retryCount - 1, completion: completion)
98
+ }
99
+ } else {
100
+ if let error = e {
101
+ print("⛔️ Did fail download, retry: \(value), \(error)")
102
+ }
103
+ self.downloadItem(key: key, value: value, retryCount: retryCount - 1, completion: completion)
104
+ }
105
+ }.resume()
106
+ }
107
+
108
+ }
@@ -0,0 +1,58 @@
1
+ enum Figma {
2
+ typealias Language = String
3
+ typealias PageId = String
4
+
5
+ struct Child: Codable {
6
+ let id: PageId
7
+ let name: String
8
+ }
9
+ struct Pages: Codable {
10
+ struct Node: Codable {
11
+ struct Document: Codable {
12
+ let name: String
13
+ let children: [Child]
14
+ }
15
+ let document: Document
16
+ }
17
+ let name: String
18
+ let nodes: [PageId: Node]
19
+ }
20
+ struct Screen {
21
+ let id: PageId
22
+ let locale: Language
23
+ let page: Int
24
+ let device: Device
25
+ }
26
+ }
27
+
28
+ extension Figma.Child {
29
+ func screen() -> Figma.Screen? {
30
+ let cmp = self.name.components(separatedBy: "/")
31
+ guard cmp.count == 4,
32
+ cmp[0] == "screen",
33
+ let device = Device(rawValue: cmp[2]),
34
+ let page = Int(cmp[3]) else { return nil }
35
+
36
+ return Figma.Screen(id: self.id, locale: cmp[1], page: page, device: device)
37
+ }
38
+ }
39
+
40
+ extension Figma.Screen {
41
+ var fileName: String {
42
+ "\(self.device.id)_\(self.page).jpg"
43
+ }
44
+ }
45
+
46
+ extension Figma.Pages {
47
+
48
+ func screens(for page: String) -> [Figma.Screen] {
49
+ var screens = [Figma.Screen]()
50
+ if let node = self.nodes[page] {
51
+ screens = node.document.children.compactMap {
52
+ $0.screen()
53
+ }
54
+ }
55
+ return screens
56
+ }
57
+
58
+ }