fastlane-plugin-wpmreleasetoolkit 1.0.0
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 +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
|