pindo 5.17.4 → 5.18.3
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/lib/pindo/base/git_handler.rb +120 -38
- data/lib/pindo/command/android/autobuild.rb +92 -31
- data/lib/pindo/command/appstore/adhocbuild.rb +1 -1
- data/lib/pindo/command/appstore/autobuild.rb +1 -1
- data/lib/pindo/command/appstore/autoresign.rb +1 -1
- data/lib/pindo/command/appstore/updateid.rb +229 -0
- data/lib/pindo/command/appstore.rb +1 -0
- data/lib/pindo/command/ios/autobuild.rb +70 -33
- data/lib/pindo/command/ios/podpush.rb +1 -1
- data/lib/pindo/command/unity/autobuild.rb +38 -18
- data/lib/pindo/command/utils/allcopyconfig.rb +144 -0
- data/lib/pindo/command/utils/copyconfig.rb +207 -0
- data/lib/pindo/command/utils/icon.rb +2 -2
- data/lib/pindo/command/utils/renewbundleid.rb +199 -0
- data/lib/pindo/command/utils/renewcert.rb +56 -54
- data/lib/pindo/command/utils.rb +3 -0
- data/lib/pindo/command/web/autobuild.rb +10 -8
- data/lib/pindo/config/build_info_manager.rb +1 -3
- data/lib/pindo/module/android/android_build_helper.rb +198 -33
- data/lib/pindo/module/android/android_config_helper.rb +305 -88
- data/lib/pindo/module/android/android_project_helper.rb +124 -14
- data/lib/pindo/module/android/android_res_helper.rb +349 -51
- data/lib/pindo/module/android/keystore_helper.rb +611 -295
- data/lib/pindo/module/android/workflow_gradle_injector.rb +702 -0
- data/lib/pindo/module/appselect.rb +4 -4
- data/lib/pindo/module/appstore/bundleid_helper.rb +204 -14
- data/lib/pindo/module/build/build_helper.rb +76 -10
- data/lib/pindo/module/build/git_repo_helper.rb +4 -4
- data/lib/pindo/module/cert/mode/base_cert_operator.rb +12 -6
- data/lib/pindo/module/pgyer/pgyerhelper.rb +124 -39
- data/lib/pindo/module/task/model/build/android_build_dev_task.rb +64 -6
- data/lib/pindo/module/task/model/git/git_commit_task.rb +70 -54
- data/lib/pindo/module/task/model/git/git_tag_task.rb +13 -9
- data/lib/pindo/module/task/model/jps/jps_upload_task.rb +110 -3
- data/lib/pindo/module/task/model/unity/unity_export_task.rb +2 -1
- data/lib/pindo/module/task/model/unity/unity_update_task.rb +2 -1
- data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +2 -1
- data/lib/pindo/module/task/model/unity_task.rb +2 -1
- data/lib/pindo/module/unity/unity_helper.rb +13 -10
- data/lib/pindo/module/unity/unity_proc_helper.rb +27 -2
- data/lib/pindo/module/xcode/applovin_xcode_helper.rb +6 -2
- data/lib/pindo/module/xcode/res/xcode_res_constant.rb +72 -0
- data/lib/pindo/module/xcode/res/xcode_res_handler.rb +3 -3
- data/lib/pindo/module/xcode/xcode_build_config.rb +46 -17
- data/lib/pindo/module/xcode/xcode_build_helper.rb +186 -25
- data/lib/pindo/module/xcode/xcode_project_helper.rb +1 -1
- data/lib/pindo/module/xcode/xcode_res_helper.rb +32 -16
- data/lib/pindo/options/groups/build_options.rb +5 -5
- data/lib/pindo/options/groups/git_options.rb +7 -5
- data/lib/pindo/options/groups/unity_options.rb +11 -0
- data/lib/pindo/options/helpers/bundleid_selector.rb +25 -0
- data/lib/pindo/options/helpers/git_constants.rb +7 -6
- data/lib/pindo/version.rb +3 -3
- metadata +12 -7
|
@@ -18,6 +18,16 @@ module Pindo
|
|
|
18
18
|
'xxxhdpi' => 192
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
+
# Android Adaptive Icon 前景层尺寸(108dp,按密度换算)
|
|
22
|
+
# 参考: Android 官方 Adaptive Icon 资源尺寸建议
|
|
23
|
+
ADAPTIVE_FOREGROUND_DENSITIES = {
|
|
24
|
+
'mdpi' => 108,
|
|
25
|
+
'hdpi' => 162,
|
|
26
|
+
'xhdpi' => 216,
|
|
27
|
+
'xxhdpi' => 324,
|
|
28
|
+
'xxxhdpi' => 432
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
21
31
|
# 创建单个密度的Android icon(同时生成方形和圆形命名)
|
|
22
32
|
# @param icon_name [String] 原始icon路径
|
|
23
33
|
# @param new_icon_dir [String] 输出目录
|
|
@@ -36,13 +46,16 @@ module Pindo
|
|
|
36
46
|
|
|
37
47
|
result = {}
|
|
38
48
|
|
|
39
|
-
#
|
|
49
|
+
# 生成两个 legacy launcher 文件:
|
|
50
|
+
# ic_launcher.png 和 ic_launcher_round.png
|
|
40
51
|
# 注意:两个都是方形,不切圆角,只是文件名不同
|
|
41
52
|
['ic_launcher.png', 'ic_launcher_round.png'].each do |filename|
|
|
42
53
|
output_path = File.join(density_dir, filename)
|
|
43
54
|
|
|
44
55
|
command = [
|
|
45
56
|
'sips',
|
|
57
|
+
# 强制输出 PNG,避免源图为 JPEG/WebP 时仅缩放仍写出 JPEG 导致扩展名与内容不符(AAPT2 报错)
|
|
58
|
+
'-s', 'format', 'png',
|
|
46
59
|
'--matchTo', '/System/Library/ColorSync/Profiles/sRGB Profile.icc',
|
|
47
60
|
'-z', size.to_s, size.to_s,
|
|
48
61
|
icon_name,
|
|
@@ -59,6 +72,30 @@ module Pindo
|
|
|
59
72
|
result[filename] = output_path
|
|
60
73
|
end
|
|
61
74
|
|
|
75
|
+
# 生成 adaptive icon 前景层(用于 mipmap-anydpi-v26/ic_launcher*.xml 引用)
|
|
76
|
+
adaptive_size = ADAPTIVE_FOREGROUND_DENSITIES[density]
|
|
77
|
+
if adaptive_size
|
|
78
|
+
foreground_filename = 'ic_launcher_foreground.png'
|
|
79
|
+
foreground_output_path = File.join(density_dir, foreground_filename)
|
|
80
|
+
foreground_command = [
|
|
81
|
+
'sips',
|
|
82
|
+
'-s', 'format', 'png',
|
|
83
|
+
'--matchTo', '/System/Library/ColorSync/Profiles/sRGB Profile.icc',
|
|
84
|
+
'-z', adaptive_size.to_s, adaptive_size.to_s,
|
|
85
|
+
icon_name,
|
|
86
|
+
'--out', foreground_output_path
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
Executable.capture_command('sips', foreground_command, capture: :out)
|
|
90
|
+
|
|
91
|
+
unless File.exist?(foreground_output_path)
|
|
92
|
+
Funlog.instance.fancyinfo_error("生成icon失败: #{foreground_output_path}")
|
|
93
|
+
return nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result[foreground_filename] = foreground_output_path
|
|
97
|
+
end
|
|
98
|
+
|
|
62
99
|
return result
|
|
63
100
|
rescue => e
|
|
64
101
|
Funlog.instance.fancyinfo_error("生成#{density}密度icon时出错: #{e.message}")
|
|
@@ -106,45 +143,63 @@ module Pindo
|
|
|
106
143
|
return success_count > 0 ? all_icons : nil
|
|
107
144
|
end
|
|
108
145
|
|
|
146
|
+
# mipmap-anydpi-v* 为 Adaptive Icon 的 layer XML 目录,清理时不可按 basename 删文件,否则会移除 ic_launcher.xml 等
|
|
147
|
+
def self.adaptive_icon_xml_resource_dir?(dir_path)
|
|
148
|
+
File.basename(dir_path.to_s).match?(/\Amipmap-anydpi-v\d+\z/i)
|
|
149
|
+
end
|
|
150
|
+
|
|
109
151
|
# 清理所有旧的 icon 文件和自定义配置
|
|
110
152
|
# @param res_dir [String] res 目录路径
|
|
153
|
+
# @param old_icon_refs [Array<Hash>] 旧 icon 引用,格式: [{ type: "mipmap", name: "xxx" }]
|
|
111
154
|
# @return [Boolean] 是否成功清理
|
|
112
|
-
def self.clean_old_icons(res_dir:nil)
|
|
155
|
+
def self.clean_old_icons(res_dir:nil, old_icon_refs: [])
|
|
113
156
|
return false unless res_dir && File.directory?(res_dir)
|
|
114
157
|
|
|
115
158
|
begin
|
|
116
159
|
cleaned_items = []
|
|
117
160
|
|
|
118
|
-
# 1.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
161
|
+
# 1. 删除配置中引用的旧 icon 文件(根据资源类型与名称精确定位)
|
|
162
|
+
configured_icon_count = 0
|
|
163
|
+
old_icon_refs.each do |ref|
|
|
164
|
+
next unless ref.is_a?(Hash)
|
|
165
|
+
|
|
166
|
+
resource_type = ref[:type].to_s
|
|
167
|
+
basename = ref[:name].to_s
|
|
168
|
+
next if resource_type.empty? || basename.empty?
|
|
169
|
+
|
|
170
|
+
candidate_dirs = Dir.glob(File.join(res_dir, "#{resource_type}-*"))
|
|
171
|
+
candidate_dirs.each do |resource_dir|
|
|
172
|
+
next unless File.directory?(resource_dir)
|
|
173
|
+
next if adaptive_icon_xml_resource_dir?(resource_dir)
|
|
174
|
+
|
|
175
|
+
configured_icon_count += remove_icon_files_with_same_basename(
|
|
176
|
+
mipmap_dir: resource_dir,
|
|
177
|
+
basename: basename
|
|
178
|
+
)
|
|
179
|
+
end
|
|
124
180
|
end
|
|
181
|
+
cleaned_items << "配置icon文件(#{configured_icon_count}个)" if configured_icon_count > 0
|
|
125
182
|
|
|
126
|
-
# 2. 删除所有 mipmap
|
|
183
|
+
# 2. 删除所有 mipmap 目录中的常见旧 icon 文件(兼容历史逻辑)
|
|
127
184
|
old_png_count = 0
|
|
128
185
|
mipmap_dirs = Dir.glob(File.join(res_dir, "mipmap-*"))
|
|
129
186
|
|
|
130
187
|
mipmap_dirs.each do |mipmap_dir|
|
|
131
188
|
next unless File.directory?(mipmap_dir)
|
|
189
|
+
next if adaptive_icon_xml_resource_dir?(mipmap_dir)
|
|
132
190
|
|
|
133
|
-
|
|
134
|
-
'app_icon
|
|
135
|
-
'app_icon_round
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'ic_launcher.png',
|
|
139
|
-
'ic_launcher_round.png'
|
|
191
|
+
old_icon_basenames = [
|
|
192
|
+
'app_icon',
|
|
193
|
+
'app_icon_round',
|
|
194
|
+
'ic_launcher',
|
|
195
|
+
'ic_launcher_round'
|
|
140
196
|
]
|
|
141
197
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
end
|
|
198
|
+
old_icon_basenames.each do |basename|
|
|
199
|
+
old_png_count += remove_icon_files_with_same_basename(
|
|
200
|
+
mipmap_dir: mipmap_dir,
|
|
201
|
+
basename: basename
|
|
202
|
+
)
|
|
148
203
|
end
|
|
149
204
|
end
|
|
150
205
|
|
|
@@ -161,28 +216,215 @@ module Pindo
|
|
|
161
216
|
end
|
|
162
217
|
end
|
|
163
218
|
|
|
164
|
-
#
|
|
219
|
+
# 确保存在标准 adaptive icon XML(Android 8+)
|
|
220
|
+
# 若项目已存在 mipmap-anydpi-v26,则保持原文件不覆盖
|
|
221
|
+
# @param res_dir [String] res 目录路径
|
|
222
|
+
# @return [Boolean] 是否成功处理
|
|
223
|
+
def self.ensure_standard_adaptive_icon_xml(res_dir:nil)
|
|
224
|
+
return false unless res_dir && File.directory?(res_dir)
|
|
225
|
+
|
|
226
|
+
begin
|
|
227
|
+
adaptive_dir = File.join(res_dir, "mipmap-anydpi-v26")
|
|
228
|
+
if Dir.exist?(adaptive_dir)
|
|
229
|
+
puts " ✓ 检测到现有 adaptive icon 配置,保持不覆盖: mipmap-anydpi-v26"
|
|
230
|
+
return true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
FileUtils.mkdir_p(adaptive_dir)
|
|
234
|
+
|
|
235
|
+
# Android 官方建议:
|
|
236
|
+
# background 使用 @color/ic_launcher_background
|
|
237
|
+
# foreground 使用 @drawable/ic_launcher_foreground
|
|
238
|
+
# 若项目缺失上述资源,则回退到可用引用,避免构建失败。
|
|
239
|
+
background_drawable = color_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_background") ?
|
|
240
|
+
"@color/ic_launcher_background" : "@android:color/white"
|
|
241
|
+
# 优先级:
|
|
242
|
+
# 1) drawable/ic_launcher_foreground(兼容已有项目)
|
|
243
|
+
# 2) mipmap/ic_launcher_foreground(本工具自动生成)
|
|
244
|
+
# 3) 回退到 legacy icon,避免构建失败
|
|
245
|
+
foreground_drawable = if drawable_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
|
|
246
|
+
"@drawable/ic_launcher_foreground"
|
|
247
|
+
elsif mipmap_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
|
|
248
|
+
"@mipmap/ic_launcher_foreground"
|
|
249
|
+
else
|
|
250
|
+
"@mipmap/ic_launcher"
|
|
251
|
+
end
|
|
252
|
+
round_foreground_drawable = if drawable_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
|
|
253
|
+
"@drawable/ic_launcher_foreground"
|
|
254
|
+
elsif mipmap_resource_exists?(res_dir: res_dir, resource_name: "ic_launcher_foreground")
|
|
255
|
+
"@mipmap/ic_launcher_foreground"
|
|
256
|
+
else
|
|
257
|
+
"@mipmap/ic_launcher_round"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
launcher_xml = <<~XML
|
|
261
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
262
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
263
|
+
<background android:drawable="#{background_drawable}" />
|
|
264
|
+
<foreground android:drawable="#{foreground_drawable}" />
|
|
265
|
+
</adaptive-icon>
|
|
266
|
+
XML
|
|
267
|
+
|
|
268
|
+
launcher_round_xml = <<~XML
|
|
269
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
270
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
271
|
+
<background android:drawable="#{background_drawable}" />
|
|
272
|
+
<foreground android:drawable="#{round_foreground_drawable}" />
|
|
273
|
+
</adaptive-icon>
|
|
274
|
+
XML
|
|
275
|
+
|
|
276
|
+
File.write(File.join(adaptive_dir, "ic_launcher.xml"), launcher_xml)
|
|
277
|
+
File.write(File.join(adaptive_dir, "ic_launcher_round.xml"), launcher_round_xml)
|
|
278
|
+
|
|
279
|
+
puts " ✓ 缺失时自动创建 adaptive icon XML: mipmap-anydpi-v26"
|
|
280
|
+
true
|
|
281
|
+
rescue => e
|
|
282
|
+
Funlog.instance.fancyinfo_error("处理 adaptive icon XML 失败: #{e.message}")
|
|
283
|
+
false
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# 检查 colors.xml 中是否声明了指定 color 资源(任意 values 变体目录)
|
|
288
|
+
# @param res_dir [String] res 目录路径
|
|
289
|
+
# @param resource_name [String] 资源名
|
|
290
|
+
# @return [Boolean]
|
|
291
|
+
def self.color_resource_exists?(res_dir:, resource_name:)
|
|
292
|
+
values_dirs = Dir.glob(File.join(res_dir, "values*")).select { |d| File.directory?(d) }
|
|
293
|
+
return false if values_dirs.empty?
|
|
294
|
+
|
|
295
|
+
values_dirs.any? do |dir|
|
|
296
|
+
colors_file = File.join(dir, "colors.xml")
|
|
297
|
+
next false unless File.file?(colors_file)
|
|
298
|
+
|
|
299
|
+
content = File.read(colors_file)
|
|
300
|
+
content.match?(/<color\s+name=["']#{Regexp.escape(resource_name)}["']/)
|
|
301
|
+
end
|
|
302
|
+
rescue
|
|
303
|
+
false
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# 检查 drawable 资源是否存在(任意 drawable 变体目录、任意常见后缀)
|
|
307
|
+
# @param res_dir [String] res 目录路径
|
|
308
|
+
# @param resource_name [String] 资源名
|
|
309
|
+
# @return [Boolean]
|
|
310
|
+
def self.drawable_resource_exists?(res_dir:, resource_name:)
|
|
311
|
+
drawable_dirs = Dir.glob(File.join(res_dir, "drawable*")).select { |d| File.directory?(d) }
|
|
312
|
+
return false if drawable_dirs.empty?
|
|
313
|
+
|
|
314
|
+
drawable_dirs.any? do |dir|
|
|
315
|
+
Dir.glob(File.join(dir, "#{resource_name}.*")).any? { |f| File.file?(f) }
|
|
316
|
+
end
|
|
317
|
+
rescue
|
|
318
|
+
false
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# 检查 mipmap 资源是否存在(任意 mipmap 变体目录、任意常见后缀)
|
|
322
|
+
# @param res_dir [String] res 目录路径
|
|
323
|
+
# @param resource_name [String] 资源名
|
|
324
|
+
# @return [Boolean]
|
|
325
|
+
def self.mipmap_resource_exists?(res_dir:, resource_name:)
|
|
326
|
+
mipmap_dirs = Dir.glob(File.join(res_dir, "mipmap*")).select { |d| File.directory?(d) }
|
|
327
|
+
return false if mipmap_dirs.empty?
|
|
328
|
+
|
|
329
|
+
mipmap_dirs.any? do |dir|
|
|
330
|
+
Dir.glob(File.join(dir, "#{resource_name}.*")).any? { |f| File.file?(f) }
|
|
331
|
+
end
|
|
332
|
+
rescue
|
|
333
|
+
false
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# 从 AndroidManifest.xml 中提取 application 的 icon 资源引用
|
|
165
337
|
# @param manifest_path [String] AndroidManifest.xml 文件路径
|
|
166
|
-
# @return [
|
|
167
|
-
def self.
|
|
168
|
-
return
|
|
338
|
+
# @return [Array<Hash>] 资源引用数组,格式: [{ type: "mipmap", name: "ic_launcher", kind: "icon" }]
|
|
339
|
+
def self.extract_icon_resource_refs_from_manifest(manifest_path:nil)
|
|
340
|
+
return [] unless manifest_path && File.exist?(manifest_path)
|
|
169
341
|
|
|
170
342
|
begin
|
|
171
343
|
require 'nokogiri'
|
|
172
344
|
|
|
173
345
|
content = File.read(manifest_path)
|
|
174
346
|
doc = Nokogiri::XML(content)
|
|
175
|
-
|
|
176
|
-
# 查找 <application> 标签
|
|
177
347
|
app_node = doc.at_xpath('//application')
|
|
178
|
-
return
|
|
348
|
+
return [] unless app_node
|
|
349
|
+
|
|
350
|
+
refs = []
|
|
351
|
+
{
|
|
352
|
+
'android:icon' => 'icon',
|
|
353
|
+
'android:roundIcon' => 'roundIcon'
|
|
354
|
+
}.each do |attr_name, kind|
|
|
355
|
+
attr_value = app_node[attr_name]
|
|
356
|
+
next unless attr_value.is_a?(String)
|
|
357
|
+
|
|
358
|
+
if (matched = attr_value.match(/^@([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)$/))
|
|
359
|
+
refs << { type: matched[1], name: matched[2], kind: kind }
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
refs.uniq { |ref| [ref[:type], ref[:name], ref[:kind]] }
|
|
364
|
+
rescue => e
|
|
365
|
+
Funlog.instance.fancyinfo_error("解析 AndroidManifest icon 配置失败: #{e.message}")
|
|
366
|
+
[]
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# 在目录内查找最匹配的 icon 文件
|
|
371
|
+
# @param source_dir [String] 目录路径
|
|
372
|
+
# @param round [Boolean] 是否查找 round icon
|
|
373
|
+
# @return [String, nil] 文件绝对路径
|
|
374
|
+
def self.find_best_icon_source_file(source_dir:, round: false)
|
|
375
|
+
return nil unless source_dir && File.directory?(source_dir)
|
|
376
|
+
|
|
377
|
+
exact_names = round ? %w[ic_launcher_round app_icon_round] : %w[ic_launcher app_icon]
|
|
378
|
+
ext_order = %w[png webp]
|
|
379
|
+
|
|
380
|
+
exact_names.each do |basename|
|
|
381
|
+
ext_order.each do |ext|
|
|
382
|
+
candidate = File.join(source_dir, "#{basename}.#{ext}")
|
|
383
|
+
return candidate if File.file?(candidate)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
179
386
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
app_node['android:roundIcon'] = '@mipmap/ic_launcher_round'
|
|
387
|
+
image_files = Dir.glob(File.join(source_dir, "*.{png,webp}")).select { |f| File.file?(f) }
|
|
388
|
+
return nil if image_files.empty?
|
|
183
389
|
|
|
184
|
-
|
|
185
|
-
|
|
390
|
+
round_match = /(?:^|[_-])(round|launcher_round|ic_round)(?:[_-]|$)/i
|
|
391
|
+
non_round_files = image_files.reject { |f| File.basename(f, File.extname(f)).match?(round_match) }
|
|
392
|
+
round_files = image_files.select { |f| File.basename(f, File.extname(f)).match?(round_match) }
|
|
393
|
+
|
|
394
|
+
selected = round ? round_files.first : non_round_files.first
|
|
395
|
+
selected || image_files.first
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# 删除目录下同名不同后缀图标(例如 ic_launcher.png / ic_launcher.webp)
|
|
399
|
+
# @param mipmap_dir [String] mipmap 目录路径
|
|
400
|
+
# @param basename [String] 图标基础名称(不带后缀)
|
|
401
|
+
# @return [Integer] 删除的文件数量
|
|
402
|
+
def self.remove_icon_files_with_same_basename(mipmap_dir:, basename:)
|
|
403
|
+
return 0 unless File.directory?(mipmap_dir)
|
|
404
|
+
return 0 if basename.nil? || basename.empty?
|
|
405
|
+
|
|
406
|
+
removed = 0
|
|
407
|
+
Dir.glob(File.join(mipmap_dir, "#{basename}.*")).each do |file_path|
|
|
408
|
+
next unless File.file?(file_path)
|
|
409
|
+
|
|
410
|
+
File.delete(file_path)
|
|
411
|
+
removed += 1
|
|
412
|
+
end
|
|
413
|
+
removed
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# 更新 AndroidManifest.xml 为标准配置
|
|
417
|
+
# @param manifest_path [String] AndroidManifest.xml 文件路径
|
|
418
|
+
# @return [Boolean] 是否成功更新
|
|
419
|
+
def self.update_manifest_to_standard(manifest_path:nil)
|
|
420
|
+
return false unless manifest_path && File.exist?(manifest_path)
|
|
421
|
+
|
|
422
|
+
begin
|
|
423
|
+
content = File.read(manifest_path)
|
|
424
|
+
updated = update_manifest_icon_attributes_in_place(content)
|
|
425
|
+
return false unless updated
|
|
426
|
+
|
|
427
|
+
File.write(manifest_path, updated)
|
|
186
428
|
|
|
187
429
|
puts " ✓ AndroidManifest.xml 已更新为标准 icon 配置"
|
|
188
430
|
|
|
@@ -193,6 +435,30 @@ module Pindo
|
|
|
193
435
|
end
|
|
194
436
|
end
|
|
195
437
|
|
|
438
|
+
def self.update_manifest_icon_attributes_in_place(xml_content)
|
|
439
|
+
icon_value = "@mipmap/ic_launcher"
|
|
440
|
+
round_value = "@mipmap/ic_launcher_round"
|
|
441
|
+
|
|
442
|
+
# 仅修改 <application ...> 开始标签内的 android:icon/android:roundIcon,避免整体格式化。
|
|
443
|
+
xml_content.sub(/<application\b[^>]*>/m) do |app_tag|
|
|
444
|
+
updated_tag = app_tag.dup
|
|
445
|
+
|
|
446
|
+
if updated_tag.match?(/\bandroid:icon\s*=\s*["'][^"']*["']/)
|
|
447
|
+
updated_tag.gsub!(/\bandroid:icon\s*=\s*(["'])[^"']*\1/, %(android:icon="#{icon_value}"))
|
|
448
|
+
else
|
|
449
|
+
updated_tag.sub!(/<application\b/, %(<application android:icon="#{icon_value}"))
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
if updated_tag.match?(/\bandroid:roundIcon\s*=\s*["'][^"']*["']/)
|
|
453
|
+
updated_tag.gsub!(/\bandroid:roundIcon\s*=\s*(["'])[^"']*\1/, %(android:roundIcon="#{round_value}"))
|
|
454
|
+
else
|
|
455
|
+
updated_tag.sub!(/<application\b/, %(<application android:roundIcon="#{round_value}"))
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
updated_tag
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
196
462
|
# 安装Android icon到项目中(完全标准化方案)
|
|
197
463
|
# @param proj_dir [String] Android项目目录
|
|
198
464
|
# @param new_icon_dir [String] icon文件目录(包含各密度文件夹)
|
|
@@ -224,10 +490,7 @@ module Pindo
|
|
|
224
490
|
FileUtils.mkdir_p(res_dir)
|
|
225
491
|
end
|
|
226
492
|
|
|
227
|
-
# 2.
|
|
228
|
-
clean_old_icons(res_dir: res_dir)
|
|
229
|
-
|
|
230
|
-
# 3. 查找并更新 AndroidManifest.xml
|
|
493
|
+
# 2. 查找 AndroidManifest.xml 并提取旧 icon 引用
|
|
231
494
|
manifest_paths = [
|
|
232
495
|
File.join(proj_dir, "launcher/src/main/AndroidManifest.xml"),
|
|
233
496
|
File.join(proj_dir, "app/src/main/AndroidManifest.xml"),
|
|
@@ -235,15 +498,21 @@ module Pindo
|
|
|
235
498
|
]
|
|
236
499
|
|
|
237
500
|
manifest_path = manifest_paths.find { |path| File.exist?(path) }
|
|
501
|
+
old_icon_refs = extract_icon_resource_refs_from_manifest(manifest_path: manifest_path)
|
|
502
|
+
|
|
503
|
+
# 3. 清理所有旧的 icon 资源
|
|
504
|
+
clean_old_icons(res_dir: res_dir, old_icon_refs: old_icon_refs)
|
|
505
|
+
|
|
506
|
+
# 4. 更新 AndroidManifest.xml 为标准 icon 配置
|
|
238
507
|
|
|
239
508
|
if manifest_path
|
|
240
509
|
update_manifest_to_standard(manifest_path: manifest_path)
|
|
241
510
|
end
|
|
242
511
|
|
|
243
|
-
#
|
|
244
|
-
update_all_xml_icon_references(proj_dir: proj_dir)
|
|
512
|
+
# 4.5 更新所有 XML 文件中的图标引用
|
|
513
|
+
update_all_xml_icon_references(proj_dir: proj_dir, old_icon_refs: old_icon_refs)
|
|
245
514
|
|
|
246
|
-
#
|
|
515
|
+
# 5. 安装标准 icon 文件
|
|
247
516
|
success_count = 0
|
|
248
517
|
densities_installed = []
|
|
249
518
|
|
|
@@ -256,23 +525,35 @@ module Pindo
|
|
|
256
525
|
# 创建目标目录
|
|
257
526
|
FileUtils.mkdir_p(target_dir) unless File.directory?(target_dir)
|
|
258
527
|
|
|
259
|
-
#
|
|
528
|
+
# 先清理同名不同后缀,避免 aapt2 Duplicate resources (png/webp 并存)
|
|
529
|
+
remove_icon_files_with_same_basename(mipmap_dir: target_dir, basename: "ic_launcher")
|
|
530
|
+
remove_icon_files_with_same_basename(mipmap_dir: target_dir, basename: "ic_launcher_round")
|
|
531
|
+
remove_icon_files_with_same_basename(mipmap_dir: target_dir, basename: "ic_launcher_foreground")
|
|
532
|
+
|
|
533
|
+
icon_source = find_best_icon_source_file(source_dir: source_dir, round: false)
|
|
534
|
+
round_source = find_best_icon_source_file(source_dir: source_dir, round: true)
|
|
535
|
+
foreground_source = find_best_icon_source_file(source_dir: source_dir, round: false)
|
|
536
|
+
explicit_foreground_png = File.join(source_dir, "ic_launcher_foreground.png")
|
|
537
|
+
explicit_foreground_webp = File.join(source_dir, "ic_launcher_foreground.webp")
|
|
538
|
+
foreground_source = explicit_foreground_png if File.file?(explicit_foreground_png)
|
|
539
|
+
foreground_source = explicit_foreground_webp if File.file?(explicit_foreground_webp)
|
|
260
540
|
files_to_copy = [
|
|
261
|
-
{
|
|
262
|
-
{
|
|
541
|
+
{ source_file: icon_source, target: 'ic_launcher.png' },
|
|
542
|
+
{ source_file: round_source, target: 'ic_launcher_round.png' },
|
|
543
|
+
{ source_file: foreground_source, target: 'ic_launcher_foreground.png' }
|
|
263
544
|
]
|
|
264
545
|
|
|
265
546
|
density_success = 0
|
|
266
547
|
files_to_copy.each do |file_map|
|
|
267
|
-
source_file =
|
|
548
|
+
source_file = file_map[:source_file]
|
|
268
549
|
target_file = File.join(target_dir, file_map[:target])
|
|
269
550
|
|
|
270
|
-
if File.exist?(source_file)
|
|
551
|
+
if source_file && File.exist?(source_file)
|
|
271
552
|
FileUtils.cp(source_file, target_file)
|
|
272
553
|
success_count += 1
|
|
273
554
|
density_success += 1
|
|
274
555
|
else
|
|
275
|
-
Funlog.instance.fancyinfo_error("源文件不存在: #{file_map[:
|
|
556
|
+
Funlog.instance.fancyinfo_error("源文件不存在: #{file_map[:target]}")
|
|
276
557
|
end
|
|
277
558
|
end
|
|
278
559
|
|
|
@@ -283,6 +564,9 @@ module Pindo
|
|
|
283
564
|
puts " ✓ 已安装标准 icon: #{densities_installed.join(', ')} (共#{success_count}个文件)"
|
|
284
565
|
end
|
|
285
566
|
|
|
567
|
+
# 6. 确保 adaptive icon XML 存在(已有配置不覆盖,缺失时补齐)
|
|
568
|
+
ensure_standard_adaptive_icon_xml(res_dir: res_dir)
|
|
569
|
+
|
|
286
570
|
return success_count > 0
|
|
287
571
|
|
|
288
572
|
rescue => e
|
|
@@ -294,8 +578,9 @@ module Pindo
|
|
|
294
578
|
|
|
295
579
|
# 更新所有 XML 文件中的图标引用(从旧名称更新为标准名称)
|
|
296
580
|
# @param proj_dir [String] Android项目目录
|
|
581
|
+
# @param old_icon_refs [Array<Hash>] 旧 icon 引用
|
|
297
582
|
# @return [Boolean] 是否成功更新
|
|
298
|
-
def self.update_all_xml_icon_references(proj_dir: nil)
|
|
583
|
+
def self.update_all_xml_icon_references(proj_dir: nil, old_icon_refs: [])
|
|
299
584
|
return false unless proj_dir && File.directory?(proj_dir)
|
|
300
585
|
|
|
301
586
|
begin
|
|
@@ -309,15 +594,28 @@ module Pindo
|
|
|
309
594
|
content = File.read(xml_file)
|
|
310
595
|
original_content = content.dup
|
|
311
596
|
|
|
312
|
-
#
|
|
597
|
+
# 替换 manifest 中已解析到的旧图标引用
|
|
598
|
+
old_icon_refs.each do |ref|
|
|
599
|
+
next unless ref.is_a?(Hash)
|
|
600
|
+
|
|
601
|
+
old_name = ref[:name].to_s
|
|
602
|
+
kind = ref[:kind].to_s
|
|
603
|
+
next if old_name.empty?
|
|
604
|
+
|
|
605
|
+
new_name = kind == "roundIcon" ? "ic_launcher_round" : "ic_launcher"
|
|
606
|
+
content.gsub!(/@mipmap\/#{Regexp.escape(old_name)}(?=["'\s>])/, "@mipmap/#{new_name}")
|
|
607
|
+
content.gsub!(/@drawable\/#{Regexp.escape(old_name)}(?=["'\s>])/, "@mipmap/#{new_name}")
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# 替换常见旧图标引用为标准名称
|
|
313
611
|
# app_icon -> ic_launcher
|
|
314
612
|
# app_icon_round -> ic_launcher_round
|
|
315
613
|
# 使用正则表达式精确匹配,避免替换 app_icon_topleft 等变体
|
|
316
614
|
# (?=["'\s>]) 表示后面必须是引号、空格或尖括号(但不包含在替换结果中)
|
|
317
615
|
content.gsub!(/@mipmap\/app_icon_round(?=["'\s>])/, '@mipmap/ic_launcher_round')
|
|
318
616
|
content.gsub!(/@mipmap\/app_icon(?=["'\s>])/, '@mipmap/ic_launcher')
|
|
319
|
-
content.gsub!(/@drawable\/app_icon_round(?=["'\s>])/, '@
|
|
320
|
-
content.gsub!(/@drawable\/app_icon(?=["'\s>])/, '@
|
|
617
|
+
content.gsub!(/@drawable\/app_icon_round(?=["'\s>])/, '@mipmap/ic_launcher_round')
|
|
618
|
+
content.gsub!(/@drawable\/app_icon(?=["'\s>])/, '@mipmap/ic_launcher')
|
|
321
619
|
|
|
322
620
|
# 如果内容有变化,保存文件
|
|
323
621
|
if content != original_content
|