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.
- checksums.yaml +4 -4
- data/Lanes/CommonFastfile +4 -8
- data/Lanes/actions/README.md +47 -0
- data/Lanes/actions/add_keys_to_lokalise.rb +106 -0
- data/Lanes/actions/android/README.md +53 -0
- data/Lanes/actions/android/lokalise_download.rb +144 -0
- data/Lanes/actions/android/lokalise_upload.rb +98 -0
- data/Lanes/actions/lokalise.rb +180 -0
- data/Lanes/actions/lokalise_metadata.rb +624 -0
- data/Lanes/actions/lokalise_upload.rb +165 -0
- data/Lanes/actions/previews.rb +157 -0
- data/Scripts/Sources/Common/Api.swift +142 -0
- data/Scripts/Sources/Common/Array.swift +8 -0
- data/Scripts/Sources/Common/Error.swift +10 -0
- data/Scripts/Sources/Common/MD5.swift +34 -0
- data/Scripts/Sources/Resources/Api+Figma.swift +43 -0
- data/Scripts/Sources/Resources/Deploy.swift +133 -0
- data/Scripts/Sources/Resources/Device.swift +42 -0
- data/Scripts/Sources/Resources/DownloadBatch.swift +108 -0
- data/Scripts/Sources/Resources/FigmaPages.swift +58 -0
- data/Scripts/Sources/Resources/Images.swift +5 -0
- data/Scripts/Sources/Resources/PreviewDownloader.swift +80 -0
- data/Scripts/Sources/Resources/ResourcesParser.swift +25 -0
- data/Scripts/Sources/Resources/ScreenshotDownloader.swift +150 -0
- data/Scripts/Sources/Resources/main.swift +58 -0
- metadata +25 -2
@@ -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
|
+
}
|