ETLane 0.1.42 → 0.1.43
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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
|
+
}
|