ETLane 0.1.42 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }