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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pindo/base/git_handler.rb +120 -38
  3. data/lib/pindo/command/android/autobuild.rb +92 -31
  4. data/lib/pindo/command/appstore/adhocbuild.rb +1 -1
  5. data/lib/pindo/command/appstore/autobuild.rb +1 -1
  6. data/lib/pindo/command/appstore/autoresign.rb +1 -1
  7. data/lib/pindo/command/appstore/updateid.rb +229 -0
  8. data/lib/pindo/command/appstore.rb +1 -0
  9. data/lib/pindo/command/ios/autobuild.rb +70 -33
  10. data/lib/pindo/command/ios/podpush.rb +1 -1
  11. data/lib/pindo/command/unity/autobuild.rb +38 -18
  12. data/lib/pindo/command/utils/allcopyconfig.rb +144 -0
  13. data/lib/pindo/command/utils/copyconfig.rb +207 -0
  14. data/lib/pindo/command/utils/icon.rb +2 -2
  15. data/lib/pindo/command/utils/renewbundleid.rb +199 -0
  16. data/lib/pindo/command/utils/renewcert.rb +56 -54
  17. data/lib/pindo/command/utils.rb +3 -0
  18. data/lib/pindo/command/web/autobuild.rb +10 -8
  19. data/lib/pindo/config/build_info_manager.rb +1 -3
  20. data/lib/pindo/module/android/android_build_helper.rb +198 -33
  21. data/lib/pindo/module/android/android_config_helper.rb +305 -88
  22. data/lib/pindo/module/android/android_project_helper.rb +124 -14
  23. data/lib/pindo/module/android/android_res_helper.rb +349 -51
  24. data/lib/pindo/module/android/keystore_helper.rb +611 -295
  25. data/lib/pindo/module/android/workflow_gradle_injector.rb +702 -0
  26. data/lib/pindo/module/appselect.rb +4 -4
  27. data/lib/pindo/module/appstore/bundleid_helper.rb +204 -14
  28. data/lib/pindo/module/build/build_helper.rb +76 -10
  29. data/lib/pindo/module/build/git_repo_helper.rb +4 -4
  30. data/lib/pindo/module/cert/mode/base_cert_operator.rb +12 -6
  31. data/lib/pindo/module/pgyer/pgyerhelper.rb +124 -39
  32. data/lib/pindo/module/task/model/build/android_build_dev_task.rb +64 -6
  33. data/lib/pindo/module/task/model/git/git_commit_task.rb +70 -54
  34. data/lib/pindo/module/task/model/git/git_tag_task.rb +13 -9
  35. data/lib/pindo/module/task/model/jps/jps_upload_task.rb +110 -3
  36. data/lib/pindo/module/task/model/unity/unity_export_task.rb +2 -1
  37. data/lib/pindo/module/task/model/unity/unity_update_task.rb +2 -1
  38. data/lib/pindo/module/task/model/unity/unity_yoo_asset_task.rb +2 -1
  39. data/lib/pindo/module/task/model/unity_task.rb +2 -1
  40. data/lib/pindo/module/unity/unity_helper.rb +13 -10
  41. data/lib/pindo/module/unity/unity_proc_helper.rb +27 -2
  42. data/lib/pindo/module/xcode/applovin_xcode_helper.rb +6 -2
  43. data/lib/pindo/module/xcode/res/xcode_res_constant.rb +72 -0
  44. data/lib/pindo/module/xcode/res/xcode_res_handler.rb +3 -3
  45. data/lib/pindo/module/xcode/xcode_build_config.rb +46 -17
  46. data/lib/pindo/module/xcode/xcode_build_helper.rb +186 -25
  47. data/lib/pindo/module/xcode/xcode_project_helper.rb +1 -1
  48. data/lib/pindo/module/xcode/xcode_res_helper.rb +32 -16
  49. data/lib/pindo/options/groups/build_options.rb +5 -5
  50. data/lib/pindo/options/groups/git_options.rb +7 -5
  51. data/lib/pindo/options/groups/unity_options.rb +11 -0
  52. data/lib/pindo/options/helpers/bundleid_selector.rb +25 -0
  53. data/lib/pindo/options/helpers/git_constants.rb +7 -6
  54. data/lib/pindo/version.rb +3 -3
  55. 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
- # 生成两个文件:ic_launcher.png ic_launcher_round.png
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. 删除 adaptive icon XML 目录
119
- adaptive_dir = File.join(res_dir, "mipmap-anydpi-v26")
120
- if Dir.exist?(adaptive_dir)
121
- xml_files = Dir.glob(File.join(adaptive_dir, "*.xml"))
122
- FileUtils.rm_rf(adaptive_dir)
123
- cleaned_items << "adaptive XML(#{xml_files.size}个)" if xml_files.any?
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 目录中的旧 icon 文件
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
- old_files = [
134
- 'app_icon.png',
135
- 'app_icon_round.png',
136
- 'ic_launcher_foreground.png',
137
- 'ic_launcher_background.png',
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
- old_files.each do |filename|
143
- file_path = File.join(mipmap_dir, filename)
144
- if File.exist?(file_path)
145
- File.delete(file_path)
146
- old_png_count += 1
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
- # 更新 AndroidManifest.xml 为标准配置
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 [Boolean] 是否成功更新
167
- def self.update_manifest_to_standard(manifest_path:nil)
168
- return false unless manifest_path && File.exist?(manifest_path)
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 false unless app_node
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
- # 设置标准 icon 配置
181
- app_node['android:icon'] = '@mipmap/ic_launcher'
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
- File.write(manifest_path, doc.to_xml)
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. 清理所有旧的 icon 资源
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
- # 3.5 更新所有 XML 文件中的图标引用
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
- # 4. 安装标准 icon 文件
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
- # 标准配置:只复制 ic_launcher.png ic_launcher_round.png
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
- { source: 'ic_launcher.png', target: 'ic_launcher.png' },
262
- { source: 'ic_launcher_round.png', target: 'ic_launcher_round.png' }
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 = File.join(source_dir, file_map[:source])
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[:source]}")
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>])/, '@drawable/ic_launcher_round')
320
- content.gsub!(/@drawable\/app_icon(?=["'\s>])/, '@drawable/ic_launcher')
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