fastlane-plugin-wpmreleasetoolkit 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +339 -0
- data/README.md +38 -0
- data/bin/drawText +19 -0
- data/ext/drawText/extconf.rb +36 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit.rb +16 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/README.md +20 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +53 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb +171 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_betabuild_prechecks.rb +103 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_prechecks.rb +83 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb +54 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_beta.rb +69 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_final_release.rb +58 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_hotfix.rb +77 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb +89 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb +68 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +115 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_finalize_prechecks.rb +71 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_hotifx_prechecks.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_merge_translators_strings.rb +106 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb +51 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_metadata.rb +52 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/circleci_trigger_job_action.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +59 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +91 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb +89 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +64 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +90 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +170 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +247 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +57 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +81 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb +96 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb +139 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +57 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_setup_action.rb +86 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_update_action.rb +139 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +134 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb +77 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb +79 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_betabuild_prechecks.rb +92 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_prechecks.rb +74 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_beta.rb +68 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_hotfix.rb +87 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_release.rb +114 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb +62 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_clear_intermediate_tags.rb +60 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_codefreeze_prechecks.rb +70 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_completecodefreeze_prechecks.rb +63 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_current_branch_is_hotfix.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_final_tag.rb +52 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_finalize_prechecks.rb +64 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb +47 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb +60 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb +121 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_hotifx_prechecks.rb +78 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +167 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_translators_strings.rb +93 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_tag_build.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb +81 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb +56 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb +46 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/an_metadata_update_helper.rb +152 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +359 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb +475 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb +91 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb +282 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/encryption_helper.rb +51 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/filesystem_helper.rb +93 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +224 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +135 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb +74 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_git_helper.rb +80 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +208 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_version_helper.rb +348 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +107 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_update_helper.rb +182 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +399 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +21 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/configuration.rb +40 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +86 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +5 -0
- metadata +449 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Fastlane
|
5
|
+
module Helper
|
6
|
+
class MetadataDownloader
|
7
|
+
attr_reader :target_folder, :target_files
|
8
|
+
|
9
|
+
def initialize(target_folder, target_files)
|
10
|
+
@target_folder = target_folder
|
11
|
+
@target_files = target_files
|
12
|
+
@alternates = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Downloads data from GlotPress,
|
16
|
+
# in JSON format
|
17
|
+
def download(target_locale, glotpress_url, is_source)
|
18
|
+
uri = URI(glotpress_url)
|
19
|
+
response = Net::HTTP.get_response(uri)
|
20
|
+
response = Net::HTTP.get_response(URI.parse(response.header['location'])) if response.code == '301'
|
21
|
+
|
22
|
+
@alternates.clear
|
23
|
+
loc_data = JSON.parse(response.body) rescue loc_data = nil
|
24
|
+
parse_data(target_locale, loc_data, is_source)
|
25
|
+
reparse_alternates(target_locale, loc_data, is_source) unless @alternates.length == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse JSON data and update the local files
|
29
|
+
def parse_data(target_locale, loc_data, is_source)
|
30
|
+
delete_existing_metadata(target_locale)
|
31
|
+
|
32
|
+
if loc_data.nil?
|
33
|
+
UI.message "No translation available for #{target_locale}"
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
loc_data.each do |d|
|
38
|
+
key = d[0].split(/\u0004/).first
|
39
|
+
source = d[0].split(/\u0004/).last
|
40
|
+
|
41
|
+
target_files.each do |file|
|
42
|
+
if file[0].to_s == key
|
43
|
+
data = file[1]
|
44
|
+
msg = is_source ? source : d[1]
|
45
|
+
update_key(target_locale, key, file, data, msg)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Parse JSON data and update the local files
|
52
|
+
def reparse_alternates(target_locale, loc_data, is_source)
|
53
|
+
loc_data.each do |d|
|
54
|
+
key = d[0].split(/\u0004/).first
|
55
|
+
source = d[0].split(/\u0004/).last
|
56
|
+
|
57
|
+
@alternates.each do |file|
|
58
|
+
puts "Data: #{file[0]} - key: #{key}"
|
59
|
+
if file[0].to_s == key
|
60
|
+
puts "Alternate: #{key}"
|
61
|
+
data = file[1]
|
62
|
+
msg = is_source ? source : d[1]
|
63
|
+
update_key(target_locale, key, file, data, msg)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def update_key(target_locale, key, file, data, msg)
|
70
|
+
message_len = msg.to_s.length - 4 # Don't count JSON delimiters.
|
71
|
+
if (data.key?(:max_size)) && (data[:max_size] != 0) && ((message_len) > data[:max_size])
|
72
|
+
if data.key?(:alternate_key)
|
73
|
+
UI.message("#{target_locale} translation for #{key} exceeds maximum length (#{message_len}). Switching to the alternate translation.")
|
74
|
+
@alternates[data[:alternate_key]] = { desc: data[:desc], max_size: 0 }
|
75
|
+
else
|
76
|
+
UI.message("Rejecting #{target_locale} traslation for #{key}: translation length: #{message_len} - max allowed length: #{data[:max_size]}")
|
77
|
+
end
|
78
|
+
else
|
79
|
+
save_metadata(target_locale, file[1][:desc], msg)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Writes the downloaded content
|
84
|
+
# to the target file
|
85
|
+
def save_metadata(locale, file_name, content)
|
86
|
+
file_path = get_target_file_path(locale, file_name)
|
87
|
+
|
88
|
+
dir_path = File.dirname(file_path)
|
89
|
+
FileUtils.mkdir_p(dir_path) unless File.exist?(dir_path)
|
90
|
+
|
91
|
+
File.open(file_path, 'w') { |file| file.puts(content) }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Some small helpers
|
95
|
+
def delete_existing_metadata(target_locale)
|
96
|
+
@target_files.each do |file|
|
97
|
+
file_path = get_target_file_path(target_locale, file[1][:desc])
|
98
|
+
File.delete(file_path) if File.exist? file_path
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_target_file_path(locale, file_name)
|
103
|
+
"#{@target_folder}/#{locale}/#{file_name}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Fastlane
|
2
|
+
module Helper
|
3
|
+
# Basic line handler
|
4
|
+
class MetadataBlock
|
5
|
+
attr_reader :block_key
|
6
|
+
|
7
|
+
def initialize(block_key)
|
8
|
+
@block_key = block_key
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_line(fw, line)
|
12
|
+
fw.puts(line) # Standard line handling: just copy
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_handler_for(key)
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class UnknownMetadataBlock < MetadataBlock
|
21
|
+
attr_reader :content_file_path
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
super(nil)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class StandardMetadataBlock < MetadataBlock
|
29
|
+
attr_reader :content_file_path
|
30
|
+
|
31
|
+
def initialize(block_key, content_file_path)
|
32
|
+
super(block_key)
|
33
|
+
@content_file_path = content_file_path
|
34
|
+
end
|
35
|
+
|
36
|
+
def is_handler_for(key)
|
37
|
+
key == @block_key.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def handle_line(fw, line)
|
41
|
+
# put the new content on block start
|
42
|
+
# and skip all the other content
|
43
|
+
generate_block(fw) if line.start_with?('msgctxt')
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_block(fw)
|
47
|
+
# init
|
48
|
+
fw.puts("msgctxt \"#{@block_key}\"")
|
49
|
+
line_count = File.foreach(@content_file_path).inject(0) { |c, _line| c + 1 }
|
50
|
+
|
51
|
+
if line_count <= 1
|
52
|
+
# Single line output
|
53
|
+
fw.puts("msgid \"#{File.open(@content_file_path, 'r').read}\"")
|
54
|
+
else
|
55
|
+
# Multiple line output
|
56
|
+
fw.puts('msgid ""')
|
57
|
+
|
58
|
+
# insert content
|
59
|
+
sf = File.open(@content_file_path, 'r').to_a
|
60
|
+
sf.each do |line|
|
61
|
+
l = "\"#{line.strip}"
|
62
|
+
l << '\\n' unless line == sf.last
|
63
|
+
l << '"'
|
64
|
+
fw.puts(l)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# close
|
69
|
+
fw.puts('msgstr ""')
|
70
|
+
fw.puts('')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class ReleaseNoteMetadataBlock < StandardMetadataBlock
|
75
|
+
attr_reader :new_key, :keep_key, :rel_note_key, :release_version
|
76
|
+
|
77
|
+
def initialize(block_key, content_file_path, release_version)
|
78
|
+
super(block_key, content_file_path)
|
79
|
+
@rel_note_key = 'release_note'
|
80
|
+
@release_version = release_version
|
81
|
+
generate_keys(release_version)
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate_keys(release_version)
|
85
|
+
values = release_version.split('.')
|
86
|
+
version_major = Integer(values[0])
|
87
|
+
version_minor = Integer(values[1])
|
88
|
+
@new_key = "#{@rel_note_key}_#{version_major}#{version_minor}"
|
89
|
+
|
90
|
+
version_major = version_major - 1 if version_minor == 0
|
91
|
+
version_minor = version_minor == 0 ? 9 : version_minor - 1
|
92
|
+
|
93
|
+
@keep_key = "#{@rel_note_key}_#{version_major}#{version_minor}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def is_handler_for(key)
|
97
|
+
values = key.split('_')
|
98
|
+
key.start_with?(@rel_note_key) && values.length == 3 && (!Integer(values[2]).nil? rescue false)
|
99
|
+
end
|
100
|
+
|
101
|
+
def handle_line(fw, line)
|
102
|
+
# put content on block start or if copying the latest one
|
103
|
+
# and skip all the other content
|
104
|
+
if line.start_with?('msgctxt')
|
105
|
+
key = extract_key(line)
|
106
|
+
@is_copying = (key == @keep_key)
|
107
|
+
generate_block(fw) if @is_copying
|
108
|
+
end
|
109
|
+
|
110
|
+
fw.puts(line) if @is_copying
|
111
|
+
end
|
112
|
+
|
113
|
+
def generate_block(fw)
|
114
|
+
# init
|
115
|
+
fw.puts("msgctxt \"#{@new_key}\"")
|
116
|
+
fw.puts('msgid ""')
|
117
|
+
fw.puts("\"#{@release_version}:\\n\"")
|
118
|
+
|
119
|
+
# insert content
|
120
|
+
File.open(@content_file_path, 'r').each do |line|
|
121
|
+
fw.puts("\"#{line.strip}\\n\"")
|
122
|
+
end
|
123
|
+
|
124
|
+
# close
|
125
|
+
fw.puts('msgstr ""')
|
126
|
+
fw.puts('')
|
127
|
+
end
|
128
|
+
|
129
|
+
def extract_key(line)
|
130
|
+
line.split(' ')[1].tr('\"', '')
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class WhatsNewMetadataBlock < StandardMetadataBlock
|
135
|
+
attr_reader :new_key, :old_key, :rel_note_key, :release_version
|
136
|
+
|
137
|
+
def initialize(block_key, content_file_path, release_version)
|
138
|
+
super(block_key, content_file_path)
|
139
|
+
@rel_note_key = 'whats_new'
|
140
|
+
@release_version = release_version
|
141
|
+
generate_keys(release_version)
|
142
|
+
end
|
143
|
+
|
144
|
+
def generate_keys(release_version)
|
145
|
+
values = release_version.split('.')
|
146
|
+
version_major = Integer(values[0])
|
147
|
+
version_minor = Integer(values[1])
|
148
|
+
@new_key = "v#{release_version}-whats-new"
|
149
|
+
|
150
|
+
version_major = version_major - 1 if version_minor == 0
|
151
|
+
version_minor = version_minor == 0 ? 9 : version_minor - 1
|
152
|
+
|
153
|
+
@old_key = "v#{version_major}.#{version_minor}-whats-new"
|
154
|
+
end
|
155
|
+
|
156
|
+
def is_handler_for(key)
|
157
|
+
key.start_with?('v') && key.end_with?('-whats-new')
|
158
|
+
end
|
159
|
+
|
160
|
+
def handle_line(fw, line)
|
161
|
+
# put content on block start or if copying the latest one
|
162
|
+
# and skip all the other content
|
163
|
+
generate_block(fw) if line.start_with?('msgctxt')
|
164
|
+
end
|
165
|
+
|
166
|
+
def generate_block(fw)
|
167
|
+
# init
|
168
|
+
fw.puts("msgctxt \"#{@new_key}\"")
|
169
|
+
fw.puts('msgid ""')
|
170
|
+
|
171
|
+
# insert content
|
172
|
+
File.open(@content_file_path, 'r').each do |line|
|
173
|
+
fw.puts("\"#{line.strip}\\n\"")
|
174
|
+
end
|
175
|
+
|
176
|
+
# close
|
177
|
+
fw.puts('msgstr ""')
|
178
|
+
fw.puts('')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,399 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
begin
|
3
|
+
$skip_magick = false
|
4
|
+
require 'RMagick'
|
5
|
+
rescue LoadError
|
6
|
+
$skip_magick = true
|
7
|
+
end
|
8
|
+
require 'json'
|
9
|
+
require 'tempfile'
|
10
|
+
require 'optparse'
|
11
|
+
require 'pathname'
|
12
|
+
require 'progress_bar'
|
13
|
+
require 'parallel'
|
14
|
+
require 'jsonlint'
|
15
|
+
require 'chroma'
|
16
|
+
require 'securerandom'
|
17
|
+
|
18
|
+
include Magick unless $skip_magick
|
19
|
+
|
20
|
+
module Fastlane
|
21
|
+
module Helper
|
22
|
+
class PromoScreenshots
|
23
|
+
def initialize
|
24
|
+
if $skip_magick
|
25
|
+
message = "PromoScreenshots feature is currently disabled.\n"
|
26
|
+
message << "Please, install RMagick if you aim to generate the PromoScreenshots.\n"
|
27
|
+
message << "\'bundle install --with screenshots\' should do it if your project is configured for PromoScreenshots.\n"
|
28
|
+
message << 'Aborting.'
|
29
|
+
UI.user_error!(message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_json(configFilePath)
|
34
|
+
configFilePath = resolve_path(configFilePath)
|
35
|
+
|
36
|
+
begin
|
37
|
+
return JSON.parse(open(configFilePath).read)
|
38
|
+
rescue
|
39
|
+
linter = JsonLint::Linter.new
|
40
|
+
linter.check(configFilePath)
|
41
|
+
linter.display_errors
|
42
|
+
|
43
|
+
UI.user_error!('Invalid JSON configuration. See errors in log.')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw_caption_to_canvas(entry, canvas, device, stylesheet_path = '')
|
48
|
+
# If no caption is provided, it's ok to skip the body of this method
|
49
|
+
return canvas if entry['text'].nil?
|
50
|
+
|
51
|
+
text = entry['text']
|
52
|
+
text_size = device['text_size']
|
53
|
+
font_size = device['font_size']
|
54
|
+
locale = entry['locale']
|
55
|
+
|
56
|
+
text = resolve_text_into_path(text, locale)
|
57
|
+
|
58
|
+
stylesheet_path = resolve_path(stylesheet_path) if can_resolve_path(stylesheet_path)
|
59
|
+
|
60
|
+
width = text_size[0]
|
61
|
+
height = text_size[1]
|
62
|
+
|
63
|
+
x_position = 0
|
64
|
+
y_position = 0
|
65
|
+
|
66
|
+
unless device['text_offset'].nil?
|
67
|
+
x_position = device['text_offset'][0]
|
68
|
+
y_position = device['text_offset'][1]
|
69
|
+
end
|
70
|
+
|
71
|
+
draw_text_to_canvas(canvas,
|
72
|
+
text,
|
73
|
+
width,
|
74
|
+
height,
|
75
|
+
x_position,
|
76
|
+
y_position,
|
77
|
+
font_size,
|
78
|
+
stylesheet_path)
|
79
|
+
end
|
80
|
+
|
81
|
+
def draw_background_to_canvas(canvas, entry)
|
82
|
+
unless entry['background'].nil?
|
83
|
+
|
84
|
+
# If we're passed an image path, let's open it and paint it to the canvas
|
85
|
+
if can_resolve_path(entry['background'])
|
86
|
+
background_image = open_image(entry['background'])
|
87
|
+
return composite_image(canvas, background_image, 0, 0)
|
88
|
+
else # Otherwise, let's assume this is a colour code
|
89
|
+
background_image = create_image(canvas.columns, canvas.rows, entry['background'])
|
90
|
+
canvas = composite_image(canvas, background_image, 0, 0)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
canvas
|
95
|
+
end
|
96
|
+
|
97
|
+
def draw_device_frame_to_canvas(device, canvas)
|
98
|
+
# Apply the device frame to the canvas, but only if one is provided
|
99
|
+
return canvas if device['device_frame_size'].nil?
|
100
|
+
|
101
|
+
w = device['device_frame_size'][0]
|
102
|
+
h = device['device_frame_size'][1]
|
103
|
+
|
104
|
+
x = 0
|
105
|
+
y = 0
|
106
|
+
|
107
|
+
unless device['device_frame_size'].nil?
|
108
|
+
x = device['device_frame_offset'][0]
|
109
|
+
y = device['device_frame_offset'][1]
|
110
|
+
end
|
111
|
+
|
112
|
+
device_frame = open_image(device['device_frame'])
|
113
|
+
device_frame = resize_image(device_frame, w, h)
|
114
|
+
composite_image(canvas, device_frame, x, y)
|
115
|
+
end
|
116
|
+
|
117
|
+
def draw_screenshot_to_canvas(entry, canvas, device)
|
118
|
+
# Don't require a screenshot to be present – we can just skip
|
119
|
+
# this function if one doesn't exist.
|
120
|
+
return canvas if entry['screenshot'].nil?
|
121
|
+
|
122
|
+
device_mask = device['screenshot_mask']
|
123
|
+
screenshot_size = device['screenshot_size']
|
124
|
+
screenshot_offset = device['screenshot_offset']
|
125
|
+
|
126
|
+
screenshot = entry['screenshot']
|
127
|
+
|
128
|
+
screenshot = open_image(screenshot)
|
129
|
+
|
130
|
+
screenshot = mask_image(screenshot, open_image(device_mask)) unless device_mask.nil?
|
131
|
+
|
132
|
+
screenshot = resize_image(screenshot, screenshot_size[0], screenshot_size[1])
|
133
|
+
composite_image(canvas, screenshot, screenshot_offset[0], screenshot_offset[1])
|
134
|
+
end
|
135
|
+
|
136
|
+
def draw_attachments_to_canvas(entry, canvas)
|
137
|
+
entry['attachments'].each do |attachment|
|
138
|
+
if !attachment['file'].nil?
|
139
|
+
canvas = draw_file_attachment_to_canvas(attachment, canvas, entry)
|
140
|
+
elsif !attachment['text'].nil?
|
141
|
+
canvas = draw_text_attachment_to_canvas(attachment, canvas, entry['locale'])
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
return canvas
|
146
|
+
end
|
147
|
+
|
148
|
+
def draw_file_attachment_to_canvas(attachment, canvas, entry)
|
149
|
+
file = resolve_path(attachment['file'])
|
150
|
+
|
151
|
+
image = open_image(file)
|
152
|
+
|
153
|
+
if attachment.member?('operations')
|
154
|
+
|
155
|
+
attachment['operations'].each do |operation|
|
156
|
+
image = apply_operation(image, operation, canvas)
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
size = attachment['size']
|
162
|
+
|
163
|
+
x_pos = attachment['position'][0]
|
164
|
+
y_pos = attachment['position'][1]
|
165
|
+
|
166
|
+
unless attachment['offset'].nil?
|
167
|
+
x_pos += attachment['offset'][0]
|
168
|
+
y_pos += attachment['offset'][1]
|
169
|
+
end
|
170
|
+
|
171
|
+
image = resize_image(image, size[0], size[1])
|
172
|
+
canvas = composite_image(canvas, image, x_pos, y_pos)
|
173
|
+
end
|
174
|
+
|
175
|
+
def draw_text_attachment_to_canvas(attachment, canvas, locale)
|
176
|
+
text = resolve_text_into_path(attachment['text'], locale)
|
177
|
+
font_size = attachment['font-size'] ||= 12
|
178
|
+
|
179
|
+
width = attachment['size'][0]
|
180
|
+
height = attachment['size'][1]
|
181
|
+
|
182
|
+
x_position = attachment['position'][0] ||= 0
|
183
|
+
y_position = attachment['position'][1] ||= 0
|
184
|
+
|
185
|
+
stylesheet_path = attachment['stylesheet']
|
186
|
+
stylesheet_path = resolve_path(stylesheet_path) if can_resolve_path(stylesheet_path)
|
187
|
+
|
188
|
+
alignment = attachment['alignment'] ||= 'center'
|
189
|
+
|
190
|
+
draw_text_to_canvas(canvas,
|
191
|
+
text,
|
192
|
+
width,
|
193
|
+
height,
|
194
|
+
x_position,
|
195
|
+
y_position,
|
196
|
+
font_size,
|
197
|
+
stylesheet_path,
|
198
|
+
alignment)
|
199
|
+
end
|
200
|
+
|
201
|
+
def apply_operation(image, operation, canvas)
|
202
|
+
case operation['type']
|
203
|
+
when 'crop'
|
204
|
+
x_pos = operation['at'][0]
|
205
|
+
y_pos = operation['at'][1]
|
206
|
+
|
207
|
+
width = operation['to'][0]
|
208
|
+
height = operation['to'][1]
|
209
|
+
|
210
|
+
crop_image(image, x_pos, y_pos, width, height)
|
211
|
+
|
212
|
+
when 'resize'
|
213
|
+
width = operation['to'][0]
|
214
|
+
height = operation['to'][1]
|
215
|
+
|
216
|
+
resize_image(image, width, height)
|
217
|
+
|
218
|
+
when 'composite'
|
219
|
+
|
220
|
+
x_pos = operation['at'][0]
|
221
|
+
y_pos = operation['at'][1]
|
222
|
+
|
223
|
+
if operation.member?('offset')
|
224
|
+
x_pos += operation['offset'][0]
|
225
|
+
y_pos += operation['offset'][1]
|
226
|
+
end
|
227
|
+
|
228
|
+
composite_image(canvas, image, x_pos, y_pos)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def draw_text_to_canvas(canvas, text, width, height, x_position, y_position, font_size, stylesheet_path, position = 'center')
|
233
|
+
begin
|
234
|
+
tempTextFile = Tempfile.new()
|
235
|
+
|
236
|
+
command = "bundle exec drawText html=\"#{text}\" maxWidth=#{width} maxHeight=#{height} output=#{tempTextFile.path} fontSize=#{font_size} stylesheet=\"#{stylesheet_path}\" alignment=\"#{position}\""
|
237
|
+
|
238
|
+
UI.crash!('Unable to draw text') unless system(command)
|
239
|
+
|
240
|
+
text_content = open_image(tempTextFile.path).trim
|
241
|
+
text_frame = create_image(width, height)
|
242
|
+
text_frame = case position
|
243
|
+
when 'left' then composite_image_left(text_frame, text_content, 0, 0)
|
244
|
+
when 'center' then composite_image_center(text_frame, text_content, 0, 0)
|
245
|
+
when 'top' then composite_image_top(text_frame, text_content, 0, 0)
|
246
|
+
end
|
247
|
+
ensure
|
248
|
+
tempTextFile.close
|
249
|
+
tempTextFile.unlink
|
250
|
+
end
|
251
|
+
|
252
|
+
composite_image(canvas, text_frame, x_position, y_position)
|
253
|
+
end
|
254
|
+
|
255
|
+
# mask_image
|
256
|
+
#
|
257
|
+
# @example
|
258
|
+
#
|
259
|
+
# image = open_image("image-path")
|
260
|
+
# mask = open_image("mask-path")
|
261
|
+
#
|
262
|
+
# mask_image(image, mask)
|
263
|
+
#
|
264
|
+
# @param [Magick::Image] image An ImageMagick object containing the image to be masked.
|
265
|
+
# @param [Magick::Image] mask An ImageMagick object containing the mask to be be applied.
|
266
|
+
#
|
267
|
+
# @return [Magick::Image] The masked image
|
268
|
+
def mask_image(image, mask, offset_x = 0, offset_y = 0)
|
269
|
+
image.composite(mask, offset_x, offset_y, CopyAlphaCompositeOp)
|
270
|
+
end
|
271
|
+
|
272
|
+
# resize_image
|
273
|
+
#
|
274
|
+
# @example
|
275
|
+
#
|
276
|
+
# image = open_image("image-path")
|
277
|
+
# resize_image(image, 640, 480)
|
278
|
+
#
|
279
|
+
# @param [Magick::Image] original An ImageMagick object containing the image to be masked.
|
280
|
+
# @param [Integer] width The new width for the image.
|
281
|
+
# @param [Integer] height The new height for the image.
|
282
|
+
#
|
283
|
+
# @return [Magick::Image] The resized image
|
284
|
+
def resize_image(original, width, height)
|
285
|
+
UI.user_error!('You must pass an image object to `resize_image`.') unless original.is_a?(Magick::Image)
|
286
|
+
|
287
|
+
original.adaptive_resize(width, height)
|
288
|
+
end
|
289
|
+
|
290
|
+
# composite_image
|
291
|
+
#
|
292
|
+
# @example
|
293
|
+
#
|
294
|
+
# image = open_image("image-path")
|
295
|
+
# other = open_image("other-path")
|
296
|
+
# composite_image(image, other, 0, 0)
|
297
|
+
#
|
298
|
+
# @param [Magick::Image] original The original image.
|
299
|
+
# @param [Magick::Image] child The image that will be placed onto the original image.
|
300
|
+
# @param [Integer] x_position The horizontal position for the image to be placed.
|
301
|
+
# @param [Integer] y_position The vertical position for the image to be placed.
|
302
|
+
#
|
303
|
+
# @return [Magick::Image] The resized image
|
304
|
+
def composite_image(original, child, x_position, y_position, starting_position = NorthWestGravity)
|
305
|
+
UI.user_error!('You must pass an image object as the first argument to `composite_image`.') unless original.is_a?(Magick::Image)
|
306
|
+
|
307
|
+
UI.user_error!('You must pass an image object as the second argument to `composite_image`.') unless child.is_a?(Magick::Image)
|
308
|
+
|
309
|
+
original.composite(child, starting_position, x_position, y_position, Magick::OverCompositeOp)
|
310
|
+
end
|
311
|
+
|
312
|
+
def composite_image_top(original, child, x_position, y_position)
|
313
|
+
composite_image(original, child, x_position, y_position, NorthGravity)
|
314
|
+
end
|
315
|
+
|
316
|
+
def composite_image_left(original, child, x_position, y_position)
|
317
|
+
composite_image(original, child, x_position, y_position, WestGravity)
|
318
|
+
end
|
319
|
+
|
320
|
+
def composite_image_center(original, child, x_position, y_position)
|
321
|
+
composite_image(original, child, x_position, y_position, CenterGravity)
|
322
|
+
end
|
323
|
+
|
324
|
+
# crop_image
|
325
|
+
#
|
326
|
+
# @example
|
327
|
+
#
|
328
|
+
# image = open_image("image-path")
|
329
|
+
# crop_image(image, other, 0, 0)
|
330
|
+
#
|
331
|
+
# @param [Magick::Image] original The original image.
|
332
|
+
# @param [Integer] x_position The horizontal position to start cropping from.
|
333
|
+
# @param [Integer] y_position The vertical position to start cropping from.
|
334
|
+
# @param [Integer] width The width of the final image.
|
335
|
+
# @param [Integer] height The height of the final image.
|
336
|
+
#
|
337
|
+
# @return [Magick::Image] The resized image
|
338
|
+
def crop_image(original, x_position, y_position, width, height)
|
339
|
+
UI.user_error!('You must pass an image object to `crop_image`.') unless original.is_a?(Magick::Image)
|
340
|
+
|
341
|
+
original.crop(x_position, y_position, width, height)
|
342
|
+
end
|
343
|
+
|
344
|
+
def open_image(path)
|
345
|
+
path = resolve_path(path)
|
346
|
+
|
347
|
+
Magick::Image.read(path) do
|
348
|
+
self.background_color = 'transparent'
|
349
|
+
end.first
|
350
|
+
end
|
351
|
+
|
352
|
+
def create_image(width, height, background = 'transparent')
|
353
|
+
background_color = background.paint.to_hex
|
354
|
+
|
355
|
+
Image.new(width, height) do
|
356
|
+
self.background_color = background
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def can_resolve_path(path)
|
361
|
+
begin
|
362
|
+
resolve_path(path)
|
363
|
+
return true
|
364
|
+
rescue
|
365
|
+
return false
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def resolve_path(path)
|
370
|
+
UI.crash!('Path not provided – you must provide one to continue') if path.nil?
|
371
|
+
|
372
|
+
[
|
373
|
+
Pathname.new(path), # Absolute Path
|
374
|
+
Pathname.new(FastlaneCore::FastlaneFolder.fastfile_path).dirname + path, # Path Relative to the fastfile
|
375
|
+
Fastlane::Helper::FilesystemHelper.plugin_root + path, # Path Relative to the plugin
|
376
|
+
Fastlane::Helper::FilesystemHelper.plugin_root + 'spec/test-data/' + path, # Path Relative to the test data
|
377
|
+
]
|
378
|
+
.each do |resolved_path|
|
379
|
+
return resolved_path if !resolved_path.nil? && resolved_path.exist?
|
380
|
+
end
|
381
|
+
|
382
|
+
message = "Unable to locate #{path}"
|
383
|
+
UI.crash!(message)
|
384
|
+
end
|
385
|
+
|
386
|
+
def resolve_text_into_path(text, locale)
|
387
|
+
localizedFile = format(text, locale)
|
388
|
+
|
389
|
+
text = if File.exist?(localizedFile)
|
390
|
+
localizedFile
|
391
|
+
elsif can_resolve_path(localizedFile)
|
392
|
+
resolve_path(localizedFile).realpath.to_s
|
393
|
+
else
|
394
|
+
format(text, 'source')
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|