solara 0.2.0 → 0.2.2

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/solara/lib/core/.DS_Store +0 -0
  3. data/solara/lib/core/aliases/alias_generator.rb +1 -0
  4. data/solara/lib/core/aliases/alias_generator_manager.rb +2 -1
  5. data/solara/lib/core/aliases/solara_terminal_setup.rb +1 -1
  6. data/solara/lib/core/brands/brand_font_switcher.rb +154 -0
  7. data/solara/lib/core/brands/brand_onboarder.rb +8 -6
  8. data/solara/lib/core/brands/brand_switcher.rb +232 -166
  9. data/solara/lib/core/brands/brands_manager.rb +15 -39
  10. data/solara/lib/core/dashboard/brand/BrandDetail.js +19 -0
  11. data/solara/lib/core/dashboard/brand/BrandDetailController.js +50 -8
  12. data/solara/lib/core/dashboard/brand/BrandDetailModel.js +9 -30
  13. data/solara/lib/core/dashboard/brand/BrandDetailView.js +49 -3
  14. data/solara/lib/core/dashboard/brand/InfoPlistStringCatalogManager.js +19 -0
  15. data/solara/lib/core/dashboard/brand/brand.html +209 -62
  16. data/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +1 -1
  17. data/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +38 -53
  18. data/solara/lib/core/dashboard/brands/BrandsController.js +6 -5
  19. data/solara/lib/core/dashboard/brands/BrandsModel.js +2 -2
  20. data/solara/lib/core/dashboard/brands/BrandsView.js +2 -2
  21. data/solara/lib/core/dashboard/brands/brands.html +3 -1
  22. data/solara/lib/core/dashboard/component/AliasesBottomSheet.js +7 -5
  23. data/solara/lib/core/dashboard/dashboard_manager.rb +2 -0
  24. data/solara/lib/core/dashboard/dashboard_server.rb +13 -8
  25. data/solara/lib/core/dashboard/handler/brand_alisases_handler.rb +4 -11
  26. data/solara/lib/core/dashboard/handler/brand_configurations_manager.rb +11 -11
  27. data/solara/lib/core/dashboard/handler/{brand_configurations_handler.rb → brand_details_handler.rb} +4 -4
  28. data/solara/lib/core/dashboard/handler/brands_handler.rb +1 -1
  29. data/solara/lib/core/dashboard/handler/edit_section_handler.rb +8 -8
  30. data/solara/lib/core/doctor/brand_doctor.rb +31 -31
  31. data/solara/lib/core/doctor/doctor_manager.rb +0 -1
  32. data/solara/lib/core/doctor/project_doctor.rb +0 -1
  33. data/solara/lib/core/doctor/schema/platform/android/android_config.json +0 -4
  34. data/solara/lib/core/doctor/schema/platform/ios/InfoPlist.xcstrings +15 -0
  35. data/solara/lib/core/doctor/schema/platform/ios/ios_config.json +0 -5
  36. data/solara/lib/core/doctor/schema/platform/shared/brand_config.json +9 -0
  37. data/solara/lib/core/doctor/schema/platform/shared/theme.json +94 -9
  38. data/solara/lib/core/doctor/validator/brand_settings_validator.rb +6 -0
  39. data/solara/lib/core/doctor/validator/brand_settings_validator_manager.rb +9 -21
  40. data/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +22 -4
  41. data/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +22 -6
  42. data/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +22 -4
  43. data/solara/lib/core/scripts/brand_config_generator.rb +1 -0
  44. data/solara/lib/core/scripts/brand_config_manager.rb +2 -4
  45. data/solara/lib/core/scripts/brand_exporter.rb +1 -1
  46. data/solara/lib/core/scripts/brand_importer.rb +2 -3
  47. data/solara/lib/core/scripts/brand_offboarder.rb +5 -0
  48. data/solara/lib/core/scripts/brand_resources_manager.rb +127 -54
  49. data/solara/lib/core/scripts/directory_creator.rb +2 -2
  50. data/solara/lib/core/scripts/file_manager.rb +53 -19
  51. data/solara/lib/core/scripts/file_path.rb +175 -30
  52. data/solara/lib/core/scripts/folder_copier.rb +3 -7
  53. data/solara/lib/core/scripts/gitignore_manager.rb +21 -10
  54. data/solara/lib/core/scripts/interactive_file_system_validator.rb +8 -2
  55. data/solara/lib/core/scripts/platform/android/android_manifest_switcher.rb +3 -3
  56. data/solara/lib/core/scripts/platform/android/android_strings_switcher.rb +26 -24
  57. data/solara/lib/core/scripts/platform/android/gradle_switcher.rb +7 -6
  58. data/solara/lib/core/scripts/platform/ios/infoplist_string_catalog_manager.rb +123 -0
  59. data/solara/lib/core/scripts/platform/ios/infoplist_switcher.rb +59 -0
  60. data/solara/lib/core/scripts/platform/ios/ios_plist_manager.rb +11 -20
  61. data/solara/lib/core/scripts/platform/ios/plist_font_manager.rb +33 -0
  62. data/solara/lib/core/scripts/platform/ios/xcconfig_generator.rb +15 -3
  63. data/solara/lib/core/scripts/platform/ios/xcode_asset_manager.rb +2 -3
  64. data/solara/lib/core/scripts/platform/ios/xcode_project_manager.rb +80 -1
  65. data/solara/lib/core/scripts/platform/ios/xcode_project_switcher.rb +15 -34
  66. data/solara/lib/core/scripts/solara_logger.rb +10 -0
  67. data/solara/lib/core/scripts/solara_settings_manager.rb +26 -1
  68. data/solara/lib/core/scripts/strings_xml_manager.rb +17 -2
  69. data/solara/lib/core/scripts/theme_generator.rb +133 -130
  70. data/solara/lib/core/scripts/yaml_manager.rb +23 -0
  71. data/solara/lib/core/solara_configurator.rb +1 -1
  72. data/solara/lib/core/template/brands/android/android_config.json +0 -1
  73. data/solara/lib/core/template/brands/android/res/values/strings.xml +4 -0
  74. data/solara/lib/core/template/brands/ios/InfoPlist.xcstrings +30 -0
  75. data/solara/lib/core/template/brands/ios/ios_config.json +1 -2
  76. data/solara/lib/core/template/brands/shared/brand_config.json +1 -0
  77. data/solara/lib/core/template/brands/shared/theme.json +3 -3
  78. data/solara/lib/core/template/config/android_template_config.json +11 -1
  79. data/solara/lib/core/template/config/flutter_template_config.json +12 -2
  80. data/solara/lib/core/template/config/ios_template_config.json +11 -1
  81. data/solara/lib/core/template/project_template_generator.rb +8 -3
  82. data/solara/lib/solara/version.rb +1 -1
  83. data/solara/lib/solara.rb +8 -4
  84. data/solara/lib/solara_initializer.rb +5 -2
  85. data/solara/lib/solara_manager.rb +1 -1
  86. metadata +62 -43
  87. data/solara/lib/core/dashboard/handler/brand_section_handler.rb +0 -20
  88. data/solara/lib/core/doctor/validator/directory_structure_validator.rb +0 -38
  89. data/solara/lib/core/doctor/validator/file_structure_validator.rb +0 -37
  90. data/solara/lib/core/scripts/platform/ios/ios_file_path_manager.rb +0 -109
  91. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/.DS_Store +0 -0
  92. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/100.png +0 -0
  93. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/102.png +0 -0
  94. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/1024.png +0 -0
  95. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/114.png +0 -0
  96. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/120.png +0 -0
  97. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/128.png +0 -0
  98. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/144.png +0 -0
  99. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/152.png +0 -0
  100. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/16.png +0 -0
  101. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/167.png +0 -0
  102. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/172.png +0 -0
  103. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/180.png +0 -0
  104. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/196.png +0 -0
  105. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/20.png +0 -0
  106. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/216.png +0 -0
  107. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/256.png +0 -0
  108. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/29.png +0 -0
  109. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/32.png +0 -0
  110. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/40.png +0 -0
  111. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/48.png +0 -0
  112. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/50.png +0 -0
  113. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/512.png +0 -0
  114. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/55.png +0 -0
  115. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/57.png +0 -0
  116. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/58.png +0 -0
  117. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/60.png +0 -0
  118. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/64.png +0 -0
  119. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/66.png +0 -0
  120. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/72.png +0 -0
  121. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/76.png +0 -0
  122. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/80.png +0 -0
  123. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/87.png +0 -0
  124. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/88.png +0 -0
  125. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/92.png +0 -0
  126. /data/solara/lib/core/template/brands/ios/{assets → xcassets}/AppIcon.appiconset/Contents.json +0 -0
@@ -6,6 +6,14 @@ module FilePath
6
6
  SolaraSettingsManager.instance.project_root
7
7
  end
8
8
 
9
+ def self.artifacts_dir_name
10
+ 'solara_artifacts'
11
+ end
12
+
13
+ def self.artifacts_dir_name_ios
14
+ 'SolaraArtifacts'
15
+ end
16
+
9
17
  def self.root
10
18
  SolaraSettingsManager.instance.root
11
19
  end
@@ -14,6 +22,14 @@ module FilePath
14
22
  "android"
15
23
  end
16
24
 
25
+ def self.test_lab
26
+ File.join(ENV['HOME'], '.solara', 'testlab', 'src')
27
+ end
28
+
29
+ def self.test_lab_cache
30
+ File.join(ENV['HOME'], '.solara', 'testlab', 'cache')
31
+ end
32
+
17
33
  def self.android_project_root
18
34
  case SolaraSettingsManager.instance.platform
19
35
  when Platform::Flutter
@@ -63,20 +79,20 @@ module FilePath
63
79
  File.join(brands, brand_key, platform)
64
80
  end
65
81
 
66
- def self.solara
67
- File.join(Dir.pwd, 'solara')
68
- end
69
-
70
82
  def self.dot_solara
71
- File.join(project_root, 'solara', '.solara')
83
+ File.join(project_root, '.solara')
72
84
  end
73
85
 
74
86
  def self.ios
75
87
  "ios"
76
88
  end
77
89
 
90
+ def self.solara_brand
91
+ File.join(project_root, 'solara', 'brand')
92
+ end
93
+
78
94
  def self.brands
79
- File.join(project_root, 'solara', 'brands')
95
+ File.join(project_root, 'solara', 'brand', 'brands')
80
96
  end
81
97
 
82
98
  def self.brand(brand_key)
@@ -91,6 +107,18 @@ module FilePath
91
107
  File.join(ios_brand_root(brand_key), 'ios_config.json')
92
108
  end
93
109
 
110
+ def self.project_infoplist_string_catalog
111
+ File.join(project_info_plist_directory, 'InfoPlist.xcstrings')
112
+ end
113
+
114
+ def self.brand_infoplist_string_catalog(brand_key)
115
+ File.join(ios_brand_root(brand_key), 'InfoPlist.xcstrings')
116
+ end
117
+
118
+ def self.brand_fonts
119
+ File.join(solara_brand, 'global', 'fonts')
120
+ end
121
+
94
122
  def self.brand_config(brand_key)
95
123
  File.join(brands, brand_key, 'shared', 'brand_config.json')
96
124
  end
@@ -127,26 +155,26 @@ module FilePath
127
155
  def self.generated_config(name, platform)
128
156
  case platform
129
157
  when Platform::Flutter
130
- File.join(flutter_artifacts, name)
158
+ File.join(flutter_lib_artifacts, name)
131
159
  when Platform::Android
132
- File.join(android_main_artifacts, name)
160
+ File.join(android_project_java_artifacts, name)
133
161
  when Platform::IOS
134
- File.join(IOSFilePathManager.instance.artifacts, name)
162
+ File.join(ios_project_root_artifacts, name)
135
163
  else
136
164
  raise ArgumentError, "Invalid platform: #{SolaraSettingsManager.instance.platform}"
137
165
  end
138
166
  end
139
167
 
140
- def self.flutter_artifacts
141
- File.join(project_root, 'lib', 'artifacts')
168
+ def self.flutter_lib_artifacts
169
+ File.join(project_root, 'lib', artifacts_dir_name)
142
170
  end
143
171
 
144
- def self.android_artifacts
145
- File.join(android_project_root, 'artifacts')
172
+ def self.android_project_root_artifacts
173
+ File.join(android_project_root, artifacts_dir_name)
146
174
  end
147
175
 
148
- def self.android_main_artifacts
149
- File.join(android_project_root, 'app', 'src', 'main', 'java', 'artifacts')
176
+ def self.android_project_java_artifacts
177
+ File.join(android_project_root, 'app', 'src', 'main', 'java', artifacts_dir_name)
150
178
  end
151
179
 
152
180
  def self.android_brand_config(brand_key)
@@ -177,28 +205,32 @@ module FilePath
177
205
  File.join(android_project_root, 'app', 'src', 'main', 'assets')
178
206
  end
179
207
 
208
+ def self.android_project_assets_artifacts
209
+ File.join(android_project_assets, artifacts_dir_name)
210
+ end
211
+
180
212
  def self.android_brand_assets(brand_key)
181
213
  File.join(android_brand_root(brand_key), 'assets')
182
214
  end
183
215
 
184
216
  def self.android_generated_properties
185
- File.join(android_artifacts, 'brand.properties')
217
+ File.join(android_project_root_artifacts, 'brand.properties')
186
218
  end
187
219
 
188
- def self.android_res
220
+ def self.android_project_res
189
221
  File.join(android_project_root, 'app', 'src', 'main', 'res')
190
222
  end
191
223
 
192
- def self.android_res_artifacts
193
- File.join(android_project_root, 'app', 'src', 'main', 'artifacts')
224
+ def self.android_project_main_artifacts
225
+ File.join(android_project_root, 'app', 'src', 'main', artifacts_dir_name)
194
226
  end
195
227
 
196
228
  def self.android_artifacts_strings
197
- File.join(android_res_artifacts, 'values', 'strings.xml')
229
+ File.join(android_project_main_artifacts, 'values', 'strings.xml')
198
230
  end
199
231
 
200
232
  def self.android_strings
201
- File.join(android_project_root, 'app', 'src', 'main', 'res', 'values', 'strings.xml')
233
+ File.join(android_project_res, 'values', 'strings.xml')
202
234
  end
203
235
 
204
236
  def self.android_brand_res(brand_key)
@@ -206,19 +238,19 @@ module FilePath
206
238
  end
207
239
 
208
240
  def self.brand_flutter_assets(brand_key)
209
- File.join(brands, brand_key, 'shared', 'assets')
241
+ File.join(brands, brand_key, 'flutter', 'assets')
210
242
  end
211
243
 
212
244
  def self.flutter_assets_artifacts
213
- File.join(project_root, 'assets', 'artifacts')
245
+ File.join(project_root, 'assets', artifacts_dir_name)
214
246
  end
215
247
 
216
248
  def self.pub_spec_yaml
217
249
  File.join(project_root, 'pubspec.yaml')
218
250
  end
219
251
 
220
- def self.flutter_artifacts_config
221
- File.join(flutter_artifacts, 'app_config.dart')
252
+ def self.flutter_lib_artifacts_config
253
+ File.join(flutter_lib_artifacts, 'app_config.dart')
222
254
  end
223
255
 
224
256
  def self.dashboard
@@ -233,10 +265,6 @@ module FilePath
233
265
  File.join(brands, 'current_brand.json')
234
266
  end
235
267
 
236
- def self.solara_template_brands(platform)
237
- File.join(solara_template, 'brands', platform)
238
- end
239
-
240
268
  def self.template_brands
241
269
  File.join(solara_template, 'brands')
242
270
  end
@@ -301,13 +329,130 @@ module FilePath
301
329
  when Platform::Android
302
330
  return android_launcher_icon(brand_key)
303
331
  when Platform::IOS
304
- path = IOSFilePathManager.instance.brand_app_icon_image(brand_key)
332
+ path = ios_brand_app_icon_image(brand_key)
305
333
  return path
306
334
  else
307
335
  raise ArgumentError, "Invalid platform: #{SolaraSettingsManager.instance.platform}"
308
336
  end
309
337
  end
310
338
 
339
+ def self.ios_brand_app_icon(brand_key)
340
+ File.join(ios_brand_xcassets(brand_key), 'AppIcon.appiconset')
341
+ end
342
+
343
+ def self.ios_brand_app_icon_image(brand_key)
344
+ appicon_set_path = ios_brand_app_icon(brand_key)
345
+
346
+ if appicon_set_path.nil?
347
+ raise "Error: AppIcon.appiconset not found for brand #{brand_key}"
348
+ end
349
+
350
+ contents_json_path = File.join(appicon_set_path, 'Contents.json')
351
+
352
+ unless File.exist?(contents_json_path)
353
+ raise "Error: Contents.json not found in AppIcon.appiconset for brand #{brand_key}"
354
+ end
355
+
356
+ contents = JSON.parse(File.read(contents_json_path))
357
+
358
+ largest_image = contents['images'].max_by do |img|
359
+ size = img['size'].scan(/(\d+)x(\d+)/).first&.map(&:to_i)
360
+ size ? size[0] * size[1] : 0
361
+ end
362
+
363
+ if largest_image
364
+ File.join(appicon_set_path, largest_image['filename'])
365
+ else
366
+ raise "No images found in Contents.json for brand #{brand_key}"
367
+ end
368
+ end
369
+
370
+ def self.app_xcconfig(name)
371
+ case SolaraSettingsManager.instance.platform
372
+ when Platform::Flutter
373
+ return File.join(project_root, ios, 'Flutter', name)
374
+ when Platform::IOS
375
+ return File.join(xcode_project_directory, 'XCConfig', name)
376
+ else
377
+ raise ArgumentError, "Invalid platform: #{SolaraSettingsManager.instance.platform}"
378
+ end
379
+ end
380
+
381
+ def self.xcode_project_directory
382
+ Pathname.new(xcode_project).parent.to_s
383
+ end
384
+
385
+ def self.xcode_project
386
+ path = ProjectSettingsManager.instance.value('xcodeproj', Platform::IOS)
387
+ File.join(project_root, path)
388
+ end
389
+
390
+ def self.ios_brand_xcassets(brand_key)
391
+ File.join(brands, brand_key, ios, 'xcassets')
392
+ end
393
+
394
+
395
+ def self.ios_brand_assets(brand_key)
396
+ File.join(brands, brand_key, ios, 'assets')
397
+ end
398
+
399
+ def self.brand_xcconfig
400
+ File.join(ios_project_root_artifacts, 'Brand.xcconfig')
401
+ end
402
+
403
+ def self.ios_project_fonts
404
+ File.join(ios_project_root_artifacts, 'Fonts')
405
+ end
406
+
407
+ def self.android_project_fonts
408
+ File.join(android_project_main_artifacts, 'font')
409
+ end
410
+
411
+ def self.flutter_project_fonts
412
+ File.join(project_root, 'assets', 'solara_fonts')
413
+ end
414
+
415
+ def self.ios_project_root_artifacts
416
+ case SolaraSettingsManager.instance.platform
417
+ when Platform::Flutter
418
+ return File.join(project_root, ios, 'Flutter', artifacts_dir_name_ios)
419
+ when Platform::IOS
420
+ return File.join(xcode_project_directory, artifacts_dir_name_ios)
421
+ else
422
+ raise ArgumentError, "Invalid platform: #{SolaraSettingsManager.instance.platform}"
423
+ end
424
+ end
425
+
426
+ def self.project_info_plist_directory
427
+ Pathname.new(info_plist).parent.to_s
428
+ end
429
+
430
+ def self.info_plist
431
+ path = ProjectSettingsManager.instance.value('Info.plist', Platform::IOS)
432
+ File.join(project_root, path)
433
+ end
434
+
435
+ def self.ios_project_assets_artifacts
436
+ File.join(File.dirname(ios_project_xcassets), 'Assets.xcassets', artifacts_dir_name_ios)
437
+ end
438
+
439
+ def self.ios_project_xcassets
440
+ path = ProjectSettingsManager.instance.value('Assets.xcassets', Platform::IOS)
441
+ File.join(project_root, path)
442
+ end
443
+
444
+ def self.ios_project_original_app_icon
445
+ File.join(ios_project_xcassets, 'AppIcon.appiconset')
446
+ end
447
+
448
+ def self.ios_project_artifacts_assets
449
+ File.join(ios_project_root_artifacts, 'Assets')
450
+ end
451
+
452
+ def self.app_xcconfig_directory
453
+ Pathname.new(app_xcconfig('Debug.xcconfig')).parent.to_s
454
+ end
455
+
311
456
  def self.schema
312
457
  File.join(root, 'core', 'doctor', 'schema')
313
458
  end
@@ -6,14 +6,10 @@ class FolderCopier
6
6
  @destination_path = destination_path
7
7
  end
8
8
 
9
- def copy(delete_if_exists: true)
10
- if File.exist?(@destination_path)
11
- if delete_if_exists
12
- FileUtils.rm_rf(@destination_path)
13
- end
14
- end
15
- FileUtils.mkdir_p(@destination_path)
9
+ def copy
10
+ FileUtils.mkdir_p(@destination_path) unless File.exist?(@destination_path)
16
11
  copy_files_and_folders(@source_path, @destination_path)
12
+ Solara.logger.debug("🚗 Copied\n\t↑ From: #{@source_path} \n\t↓ To: #{@destination_path}")
17
13
  end
18
14
 
19
15
  private
@@ -4,16 +4,27 @@ class GitignoreManager
4
4
  create_gitignore_if_not_exists
5
5
  end
6
6
 
7
- def self.ignore_redundant_files
8
- GitignoreManager.new(FilePath.project_root)
9
- .add_items([
10
- "# Generated by Solara. Ignore redundant brand specific changes.",
11
- "**/artifacts/",
12
- "**/Artifacts/",
13
- "solara/brands/current_app.json",
14
- "solara/.solara/aliases/",
15
- "solara/.solara/solara_settings.json",
16
- ])
7
+ def self.ignore
8
+ Solara.logger.start_step("Exclude Brand-Generated Files and Folders from Git")
9
+
10
+ items = [
11
+ "# Generated by Solara. Ignore redundant brand specific changes.",
12
+ "**/#{FilePath.artifacts_dir_name}/",
13
+ "**/#{FilePath.artifacts_dir_name_ios}/",
14
+ "solara/brand/brands/current_brand.json",
15
+ ".solara/aliases/",
16
+ ".solara/solara_settings.json",
17
+ "solara_fonts",
18
+ ]
19
+
20
+ if Platform.is_flutter || Platform.is_ios
21
+ items << FileManager.get_relative_path_to_root(FilePath.project_infoplist_string_catalog)
22
+ # The excluded InfoPlist.xcstrings maybe at the root. In this case we have to avoid ignoring the brands files.
23
+ items << '!solara/brand/brands/**/InfoPlist.xcstrings'
24
+ end
25
+
26
+ GitignoreManager.new(FilePath.project_root).add_items(items)
27
+ Solara.logger.end_step("Exclude Brand-Generated Files and Folders from Git")
17
28
  end
18
29
 
19
30
  def add_items(items)
@@ -34,13 +34,19 @@ class InteractiveFileSystemValidator
34
34
  validate_required_item(item)
35
35
  end
36
36
  else
37
- ignored = %w[solara/ Artifacts/ Pods/ build/]
37
+ ignored = [
38
+ 'macos/',
39
+ 'solara/',
40
+ "#{FilePath.artifacts_dir_name}/",
41
+ "#{FilePath.artifacts_dir_name_ios}/",
42
+ 'Pods/',
43
+ 'build/']
38
44
 
39
45
  root = File.join(@project_root, item_path)
40
46
  paths = if recursive
41
47
  FileManager.find_files_by_name(root, name)
42
48
  else
43
- Dir.glob(File.join(root, name)).select { |path| File.file?(path) || File.directory?(path) }
49
+ FileManager.find_files_and_directories(root, name)
44
50
  end
45
51
 
46
52
  paths = paths.map { |path| FileManager.get_relative_path(@project_root, path) }
@@ -2,8 +2,8 @@ class AndroidManifestSwitcher
2
2
  def initialize
3
3
  end
4
4
 
5
- def update_manifest(config)
6
- Solara.logger.start_step("Update AndroidManifest")
5
+ def switch(config)
6
+ Solara.logger.start_step("Switch AndroidManifest")
7
7
  manifest_file = FilePath.android_manifest
8
8
  if File.exist?(manifest_file)
9
9
  manifest_content = File.read(manifest_file)
@@ -13,7 +13,7 @@ class AndroidManifestSwitcher
13
13
  else
14
14
  Solara.logger.debug("❌ #{FilePath.android_manifest} not found. Skipping manifest update.")
15
15
  end
16
- Solara.logger.end_step("Update AndroidManifest")
16
+ Solara.logger.end_step("Switch AndroidManifest")
17
17
  end
18
18
 
19
19
  private
@@ -1,39 +1,41 @@
1
1
  class AndroidStringsSwitcher
2
- def initialize
2
+ def initialize(brand_key)
3
+ @brand_key = brand_key
4
+ end
5
+
6
+ def switch
7
+ Solara.logger.log_step("Switch strings.xml") do
8
+ switch_brand_name
9
+ remove_original_app_name_from_strings
10
+ end
3
11
  end
4
12
 
5
- def update(config)
6
- Solara.logger.start_step("Generate artifacts/strings.xml")
7
- strings_file = FilePath.android_artifacts_strings
13
+ private
8
14
 
9
- # Create the file if it doesn't exist
10
- unless File.exist?(strings_file)
11
- FileUtils.mkdir_p(File.dirname(strings_file))
12
- end
15
+ def switch_brand_name
16
+ brand_config_path = FilePath.brand_config(@brand_key)
17
+ brand_config = JSON.parse(File.read(brand_config_path))
18
+ strings_path = FilePath.android_artifacts_strings
19
+ manager = StringsXmlManager.new(strings_path)
20
+ app_name_value = manager.get_value('app_name')
13
21
 
14
- strings_content = generate_strings_xml_content(config)
15
- File.write(strings_file, strings_content)
16
- Solara.logger.debug("Updated #{strings_file} with name: \"#{config['brandName']}\"")
22
+ # If user has specified a brandName in brand_config, we don't override it.
23
+ if !app_name_value.nil? && !app_name_value.empty?
24
+ Solara.logger.debug("App name is specified in #{strings_path}. Skipping updating from #{brand_config_path}!")
25
+ return
26
+ end
17
27
 
18
- remove_app_name_from_strings
19
- Solara.logger.end_step("Generate artifacts/strings.xml")
28
+ manager.update_string_value('app_name', brand_config['brandName'])
29
+ Solara.logger.debug("Updated app anme in #{strings_path} from #{brand_config_path}.")
20
30
  end
21
31
 
22
32
  # It's important to delete app_name to avoid duplicate resources
23
- def remove_app_name_from_strings
33
+ def remove_original_app_name_from_strings
24
34
  file_path = FilePath.android_strings
35
+ return unless File.exist?(file_path)
36
+
25
37
  manager = StringsXmlManager.new(file_path)
26
38
  manager.delete_app_name
27
39
  end
28
40
 
29
- private
30
-
31
- def generate_strings_xml_content(config)
32
- <<-XML
33
- <?xml version="1.0" encoding="utf-8"?>
34
- <resources>
35
- <string name="app_name">#{config['brandName']}</string>
36
- </resources>
37
- XML
38
- end
39
41
  end
@@ -1,3 +1,4 @@
1
+ Dir[File.expand_path('../../*.rb', __dir__)].each { |file| require_relative file }
1
2
  require 'fileutils'
2
3
  require 'json'
3
4
 
@@ -10,14 +11,14 @@ import java.util.Properties
10
11
 
11
12
  KOTLIN_PROPERTIES_LOADER = <<-KOTLIN
12
13
  val brandProperties = Properties().apply {
13
- load(FileInputStream(file("../artifacts/brand.properties")))
14
+ load(FileInputStream(file("../#{FilePath.artifacts_dir_name}/brand.properties")))
14
15
  }
15
16
  KOTLIN
16
17
 
17
18
  GROOVY_PROPERTIES_LOADER = <<-GROOVY
18
19
  project.ext {
19
20
  brandProperties = new Properties()
20
- brandProperties.load(new FileInputStream(file("../artifacts/brand.properties")))
21
+ brandProperties.load(new FileInputStream(file("../#{FilePath.artifacts_dir_name}/brand.properties")))
21
22
  }
22
23
  GROOVY
23
24
 
@@ -30,7 +31,7 @@ project.ext {
30
31
  KOTLIN_VERSION_CODE = 'versionCode = brandProperties.getProperty("versionCode").toInt()'
31
32
  GROOVY_VERSION_CODE = "versionCode = project.ext.brandProperties.getProperty('versionCode').toInteger()"
32
33
 
33
- DEFAULT_SOURCE_SETS = %w[src/main/res src/main/artifacts]
34
+ DEFAULT_SOURCE_SETS = ['src/main/res', "src/main/#{FilePath.artifacts_dir_name}"]
34
35
 
35
36
  def initialize(brand_key)
36
37
  @brand_key = brand_key
@@ -39,15 +40,15 @@ project.ext {
39
40
  @source_sets = (@brand_config['sourceSets'] || []).concat(DEFAULT_SOURCE_SETS).uniq
40
41
  end
41
42
 
42
- def update_build_gradle
43
- Solara.logger.start_step("Update app/build.gradle")
43
+ def switch
44
+ Solara.logger.start_step("Switch app/build.gradle")
44
45
  gradle_file = FilePath.android_app_gradle
45
46
  gradle_content = File.read(gradle_file)
46
47
 
47
48
  update_gradle(gradle_file, gradle_content)
48
49
  add_source_sets(gradle_file)
49
50
  update_keystore_config(gradle_file)
50
- Solara.logger.end_step("Update app/build.gradle")
51
+ Solara.logger.end_step("Switch app/build.gradle")
51
52
  end
52
53
 
53
54
  private
@@ -0,0 +1,123 @@
1
+ require 'json'
2
+
3
+ module StringCatalogUtils
4
+ def load_string_catalog(path)
5
+ JSON.parse(File.read(path))
6
+ end
7
+
8
+ def get_value(data, key, target, language)
9
+ lang = language || data['sourceLanguage']
10
+ data['strings'][key]['localizations'][lang]['stringUnit'][target]
11
+ end
12
+
13
+ def has_value?(data, key, language)
14
+ state = get_value(data, key, 'state', language)
15
+ value = get_value(data, key, 'value', language)
16
+ state != 'new' && value != ''
17
+ end
18
+ end
19
+
20
+ class InfoPListStringCatalogManager
21
+ include StringCatalogUtils
22
+
23
+ def initialize(string_catalog_path)
24
+ @string_catalog_path = string_catalog_path
25
+ @data = load_string_catalog(@string_catalog_path)
26
+ end
27
+
28
+ def update(result, can_remove_extra_values: true)
29
+
30
+ update_localizations(result)
31
+
32
+ remove_unused_localizations(result) if can_remove_extra_values
33
+
34
+ File.write(@string_catalog_path, JSON.pretty_generate(@data))
35
+ end
36
+
37
+ def source_language
38
+ @data['sourceLanguage']
39
+ end
40
+
41
+ def get(key, language: nil)
42
+ get_value(@data, key, 'value', language)
43
+ end
44
+
45
+ private
46
+
47
+ def update_localizations(result)
48
+ result.each do |base_key, languages|
49
+ next unless @data['strings'].key?(base_key)
50
+
51
+ languages.each do |lang_code, value|
52
+ initialize_localization(base_key, lang_code)
53
+ update_string_unit(base_key, lang_code, value)
54
+ end
55
+ end
56
+ end
57
+
58
+ def initialize_localization(base_key, lang_code)
59
+ @data['strings'][base_key]['localizations'][lang_code] ||= {
60
+ "stringUnit" => {
61
+ "state" => "new",
62
+ "value" => ""
63
+ }
64
+ }
65
+ end
66
+
67
+ def update_string_unit(base_key, lang_code, value)
68
+ state = value.empty? ? 'new' : 'translated'
69
+ @data['strings'][base_key]['localizations'][lang_code]['stringUnit'].merge!({
70
+ 'state' => state,
71
+ 'value' => value
72
+ })
73
+ end
74
+
75
+ def remove_unused_localizations(result)
76
+ @data['strings'].each do |base_key, data|
77
+ data['localizations'].each_key do |lang_code|
78
+ data['localizations'].delete(lang_code) unless result.dig(base_key, lang_code)
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ class InfoPListStringCatalogValidator
85
+ include StringCatalogUtils
86
+
87
+ def initialize(string_catalog_path)
88
+ @string_catalog_path = string_catalog_path
89
+ @data = load_string_catalog(@string_catalog_path)
90
+ @source_language = @data['sourceLanguage']
91
+ end
92
+
93
+ def validate
94
+ issues = []
95
+
96
+ @data['strings'].keys.each do |key|
97
+ languages = @data['strings'][key]['localizations'].keys
98
+ languages.each do |language|
99
+ validation_error = validate_key(key, language)
100
+ if validation_error
101
+ issues << (language == @source_language ? Issue.error(validation_error) : Issue.warning(validation_error))
102
+ end
103
+ end
104
+ end
105
+
106
+ issues
107
+ end
108
+
109
+ private
110
+
111
+ def validate_key(key, language)
112
+ return nil if has_value?(@data, key, language)
113
+
114
+ "The value for '#{key}' is not translated in #{@string_catalog_path}. " \
115
+ "To resolve this issue, please open the dashboard and update the entry for '#{key}.#{@source_language}' in section 'iOS InfoPlist.xcstrings Configuration'. " \
116
+ "If you prefer, you can manually add a value and mark its state as 'translated'."
117
+ end
118
+ end
119
+
120
+ module InfoPListKey
121
+ BUNDLE_NAME = 'CFBundleName'
122
+ BUNDLE_DISPLAY_NAME = 'CFBundleDisplayName'
123
+ end