solara 0.1.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.
Files changed (175) hide show
  1. checksums.yaml +7 -0
  2. data/bin/solara +18 -0
  3. data/solara/lib/.DS_Store +0 -0
  4. data/solara/lib/core/.DS_Store +0 -0
  5. data/solara/lib/core/aliases/alias_generator.rb +128 -0
  6. data/solara/lib/core/aliases/alias_generator_manager.rb +28 -0
  7. data/solara/lib/core/aliases/solara_terminal_setup.rb +103 -0
  8. data/solara/lib/core/brands/brand_onboarder.rb +46 -0
  9. data/solara/lib/core/brands/brand_switcher.rb +204 -0
  10. data/solara/lib/core/brands/brands_manager.rb +154 -0
  11. data/solara/lib/core/dashboard/.DS_Store +0 -0
  12. data/solara/lib/core/dashboard/brand/.DS_Store +0 -0
  13. data/solara/lib/core/dashboard/brand/BrandDetail.js +11 -0
  14. data/solara/lib/core/dashboard/brand/BrandDetailController.js +361 -0
  15. data/solara/lib/core/dashboard/brand/BrandDetailModel.js +155 -0
  16. data/solara/lib/core/dashboard/brand/BrandDetailView.js +245 -0
  17. data/solara/lib/core/dashboard/brand/brand.html +477 -0
  18. data/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +123 -0
  19. data/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +260 -0
  20. data/solara/lib/core/dashboard/brands/Brands.js +10 -0
  21. data/solara/lib/core/dashboard/brands/BrandsController.js +155 -0
  22. data/solara/lib/core/dashboard/brands/BrandsModel.js +136 -0
  23. data/solara/lib/core/dashboard/brands/BrandsView.js +136 -0
  24. data/solara/lib/core/dashboard/brands/brands.html +345 -0
  25. data/solara/lib/core/dashboard/component/AddFieldSheet.js +212 -0
  26. data/solara/lib/core/dashboard/component/AliasesBottomSheet.js +128 -0
  27. data/solara/lib/core/dashboard/component/BrandOptionsBottomSheet.js +130 -0
  28. data/solara/lib/core/dashboard/component/ConfirmationDialog.js +103 -0
  29. data/solara/lib/core/dashboard/component/MessageBottomSheet.js +80 -0
  30. data/solara/lib/core/dashboard/component/OnboardBrandBottomSheet.js +214 -0
  31. data/solara/lib/core/dashboard/dashboard_manager.rb +19 -0
  32. data/solara/lib/core/dashboard/dashboard_server.rb +132 -0
  33. data/solara/lib/core/dashboard/handler/base_handler.rb +25 -0
  34. data/solara/lib/core/dashboard/handler/brand_alisases_handler.rb +33 -0
  35. data/solara/lib/core/dashboard/handler/brand_configurations_handler.rb +18 -0
  36. data/solara/lib/core/dashboard/handler/brand_configurations_manager.rb +73 -0
  37. data/solara/lib/core/dashboard/handler/brand_icon_handler.rb +20 -0
  38. data/solara/lib/core/dashboard/handler/brand_section_handler.rb +20 -0
  39. data/solara/lib/core/dashboard/handler/brands_handler.rb +14 -0
  40. data/solara/lib/core/dashboard/handler/current_brand_handler.rb +18 -0
  41. data/solara/lib/core/dashboard/handler/doctor_handler.rb +39 -0
  42. data/solara/lib/core/dashboard/handler/edit_section_handler.rb +55 -0
  43. data/solara/lib/core/dashboard/handler/offboard_brand_handler.rb +34 -0
  44. data/solara/lib/core/dashboard/handler/onboard_brand_handler.rb +53 -0
  45. data/solara/lib/core/dashboard/handler/redirect_handler.rb +12 -0
  46. data/solara/lib/core/dashboard/handler/switch_handler.rb +25 -0
  47. data/solara/lib/core/dashboard/index.html +36 -0
  48. data/solara/lib/core/dashboard/local.html +41 -0
  49. data/solara/lib/core/dashboard/res/favicon/android-chrome-192x192.png +0 -0
  50. data/solara/lib/core/dashboard/res/favicon/android-chrome-512x512.png +0 -0
  51. data/solara/lib/core/dashboard/res/favicon/apple-touch-icon.png +0 -0
  52. data/solara/lib/core/dashboard/res/favicon/favicon-16x16.png +0 -0
  53. data/solara/lib/core/dashboard/res/favicon/favicon-32x32.png +0 -0
  54. data/solara/lib/core/dashboard/res/favicon/favicon.ico +0 -0
  55. data/solara/lib/core/dashboard/res/favicon/site.webmanifest +1 -0
  56. data/solara/lib/core/dashboard/solara.png +0 -0
  57. data/solara/lib/core/doctor/brand_doctor.rb +94 -0
  58. data/solara/lib/core/doctor/doctor_manager.rb +35 -0
  59. data/solara/lib/core/doctor/project_doctor.rb +8 -0
  60. data/solara/lib/core/doctor/schema/brand_configurations.json +60 -0
  61. data/solara/lib/core/doctor/schema/platform/android/android_config.json +23 -0
  62. data/solara/lib/core/doctor/schema/platform/android/android_signing.json +23 -0
  63. data/solara/lib/core/doctor/schema/platform/ios/ios_config.json +27 -0
  64. data/solara/lib/core/doctor/schema/platform/ios/ios_signing.json +27 -0
  65. data/solara/lib/core/doctor/schema/platform/shared/theme.json +48 -0
  66. data/solara/lib/core/doctor/validator/brand_settings_validator.rb +55 -0
  67. data/solara/lib/core/doctor/validator/brand_settings_validator_manager.rb +82 -0
  68. data/solara/lib/core/doctor/validator/directory_structure_validator.rb +38 -0
  69. data/solara/lib/core/doctor/validator/file_structure_validator.rb +37 -0
  70. data/solara/lib/core/doctor/validator/json_file_validator.rb +21 -0
  71. data/solara/lib/core/doctor/validator/json_schema_validator.rb +32 -0
  72. data/solara/lib/core/doctor/validator/project_filesystem_validator.rb +70 -0
  73. data/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +51 -0
  74. data/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +53 -0
  75. data/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +51 -0
  76. data/solara/lib/core/doctor/validator/template/template_validator.rb +108 -0
  77. data/solara/lib/core/doctor/validator/validation_strategy.rb +7 -0
  78. data/solara/lib/core/scripts/brand_config_generator.rb +245 -0
  79. data/solara/lib/core/scripts/brand_config_manager.rb +90 -0
  80. data/solara/lib/core/scripts/brand_exporter.rb +38 -0
  81. data/solara/lib/core/scripts/brand_importer.rb +84 -0
  82. data/solara/lib/core/scripts/brand_offboarder.rb +19 -0
  83. data/solara/lib/core/scripts/brand_resources_manager.rb +77 -0
  84. data/solara/lib/core/scripts/directory_creator.rb +22 -0
  85. data/solara/lib/core/scripts/file_manager.rb +90 -0
  86. data/solara/lib/core/scripts/file_path.rb +327 -0
  87. data/solara/lib/core/scripts/folder_copier.rb +41 -0
  88. data/solara/lib/core/scripts/gitignore_manager.rb +54 -0
  89. data/solara/lib/core/scripts/interactive_file_system_validator.rb +110 -0
  90. data/solara/lib/core/scripts/platform/android/android_manifest_switcher.rb +24 -0
  91. data/solara/lib/core/scripts/platform/android/android_strings_switcher.rb +39 -0
  92. data/solara/lib/core/scripts/platform/android/gradle_switcher.rb +233 -0
  93. data/solara/lib/core/scripts/platform/android/properties_generator.rb +31 -0
  94. data/solara/lib/core/scripts/platform/ios/ios_file_path_manager.rb +109 -0
  95. data/solara/lib/core/scripts/platform/ios/ios_plist_manager.rb +42 -0
  96. data/solara/lib/core/scripts/platform/ios/xcconfig_generator.rb +44 -0
  97. data/solara/lib/core/scripts/platform/ios/xcode_asset_manager.rb +56 -0
  98. data/solara/lib/core/scripts/platform/ios/xcode_project_manager.rb +82 -0
  99. data/solara/lib/core/scripts/platform/ios/xcode_project_switcher.rb +130 -0
  100. data/solara/lib/core/scripts/project_settings_manager.rb +39 -0
  101. data/solara/lib/core/scripts/solara_logger.rb +103 -0
  102. data/solara/lib/core/scripts/solara_settings_manager.rb +73 -0
  103. data/solara/lib/core/scripts/solara_status_manager.rb +55 -0
  104. data/solara/lib/core/scripts/solara_version_manager.rb +42 -0
  105. data/solara/lib/core/scripts/strings_xml_manager.rb +22 -0
  106. data/solara/lib/core/scripts/terminal_input_manager.rb +22 -0
  107. data/solara/lib/core/scripts/theme_generator.rb +250 -0
  108. data/solara/lib/core/scripts/yaml_manager.rb +72 -0
  109. data/solara/lib/core/solara_configurator.rb +15 -0
  110. data/solara/lib/core/template/brands/android/android_config.json +8 -0
  111. data/solara/lib/core/template/brands/android/android_signing.json +6 -0
  112. data/solara/lib/core/template/brands/android/res/.DS_Store +0 -0
  113. data/solara/lib/core/template/brands/android/res/mipmap-hdpi/ic_launcher.png +0 -0
  114. data/solara/lib/core/template/brands/android/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  115. data/solara/lib/core/template/brands/android/res/mipmap-mdpi/ic_launcher.png +0 -0
  116. data/solara/lib/core/template/brands/android/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  117. data/solara/lib/core/template/brands/android/res/mipmap-xhdpi/ic_launcher.png +0 -0
  118. data/solara/lib/core/template/brands/android/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  119. data/solara/lib/core/template/brands/android/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  120. data/solara/lib/core/template/brands/android/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  121. data/solara/lib/core/template/brands/android/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  122. data/solara/lib/core/template/brands/android/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  123. data/solara/lib/core/template/brands/brands.json +4 -0
  124. data/solara/lib/core/template/brands/ios/assets/.DS_Store +0 -0
  125. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/100.png +0 -0
  126. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/102.png +0 -0
  127. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/1024.png +0 -0
  128. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/114.png +0 -0
  129. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/120.png +0 -0
  130. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/128.png +0 -0
  131. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/144.png +0 -0
  132. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/152.png +0 -0
  133. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/16.png +0 -0
  134. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/167.png +0 -0
  135. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/172.png +0 -0
  136. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/180.png +0 -0
  137. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/196.png +0 -0
  138. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/20.png +0 -0
  139. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/216.png +0 -0
  140. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/256.png +0 -0
  141. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/29.png +0 -0
  142. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/32.png +0 -0
  143. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/40.png +0 -0
  144. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/48.png +0 -0
  145. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/50.png +0 -0
  146. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/512.png +0 -0
  147. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/55.png +0 -0
  148. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/57.png +0 -0
  149. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/58.png +0 -0
  150. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/60.png +0 -0
  151. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/64.png +0 -0
  152. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/66.png +0 -0
  153. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/72.png +0 -0
  154. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/76.png +0 -0
  155. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/80.png +0 -0
  156. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/87.png +0 -0
  157. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/88.png +0 -0
  158. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/92.png +0 -0
  159. data/solara/lib/core/template/brands/ios/assets/AppIcon.appiconset/Contents.json +1 -0
  160. data/solara/lib/core/template/brands/ios/ios_config.json +7 -0
  161. data/solara/lib/core/template/brands/ios/ios_signing.json +7 -0
  162. data/solara/lib/core/template/brands/shared/.DS_Store +0 -0
  163. data/solara/lib/core/template/brands/shared/brand_config.json +2 -0
  164. data/solara/lib/core/template/brands/shared/theme.json +46 -0
  165. data/solara/lib/core/template/config/android_template_config.json +57 -0
  166. data/solara/lib/core/template/config/flutter_template_config.json +62 -0
  167. data/solara/lib/core/template/config/ios_template_config.json +57 -0
  168. data/solara/lib/core/template/project_template_generator.rb +63 -0
  169. data/solara/lib/platform_detector.rb +84 -0
  170. data/solara/lib/solara/cli.rb +5 -0
  171. data/solara/lib/solara/version.rb +3 -0
  172. data/solara/lib/solara.rb +238 -0
  173. data/solara/lib/solara_initializer.rb +44 -0
  174. data/solara/lib/solara_manager.rb +73 -0
  175. metadata +346 -0
@@ -0,0 +1,110 @@
1
+ Dir.glob("#{__dir__}/*.rb").each { |file| require file }
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ class InteractiveFileSystemValidator
7
+ def initialize(project_root, settings_manager)
8
+ @project_root = project_root
9
+ @settings_manager = settings_manager
10
+ end
11
+
12
+ def start(file_system)
13
+ validate(file_system)
14
+ end
15
+
16
+ def validate(file_system)
17
+ file_system.each do |item|
18
+ key = item[:key]
19
+ name = item[:name]
20
+ type = item[:type]
21
+ platform = item[:platform]
22
+ item_path = item.fetch(:path, '')
23
+ recursive = item.fetch(:recursive, true)
24
+
25
+ value = @settings_manager.value(key, platform)
26
+
27
+ if value
28
+ # Check if the item exists and is of the correct type
29
+ if type == 'file' && !File.file?(value)
30
+ Solara.logger.failure("Missing file: #{key} (#{value})")
31
+ validate_required_item(item)
32
+ elsif type == 'folder' && !File.directory?(value)
33
+ Solara.logger.failure("Missing folder: #{key} (#{value})")
34
+ validate_required_item(item)
35
+ end
36
+ else
37
+ ignored = %w[solara/ Artifacts/ Pods/ build/]
38
+
39
+ root = File.join(@project_root, item_path)
40
+ paths = if recursive
41
+ FileManager.find_files_by_name(root, name)
42
+ else
43
+ Dir.glob(File.join(root, name)).select { |path| File.file?(path) || File.directory?(path) }
44
+ end
45
+
46
+ paths = paths.map { |path| FileManager.get_relative_path(@project_root, path) }
47
+ .reject { |path| ignored.any? { |ignored_path| path.include?(ignored_path) } }
48
+
49
+ case paths.size
50
+ when 0
51
+ Solara.logger.failure("Missing #{type}: #{key}")
52
+ validate_required_item(item)
53
+ when 1
54
+ @settings_manager.add(key, paths.first, platform)
55
+
56
+ Solara.logger.debug("Added #{type}: #{key} (#{paths.first})")
57
+ else
58
+ Solara.logger.failure("Found multiple paths for #{key}:\n\t- #{paths.join("\n\t- ")}")
59
+ validate_required_item(item)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def validate_required_item(item)
68
+ item_key = item[:key]
69
+ item_type = item[:type]
70
+ platform = item[:platform]
71
+ item_path = ''
72
+
73
+ loop do
74
+ item_path = get_path_from_user(item_key, item_type)
75
+ break if validate_item(item_path, item_key, item_type)
76
+ end
77
+ value = FileManager.get_relative_path(@project_root, item_path)
78
+ @settings_manager.add(item_key, value, platform)
79
+ end
80
+
81
+ def validate_item(item_path, item_name, item_type)
82
+ return false if item_path.nil? || !item_path.end_with?(item_name)
83
+
84
+ case item_type
85
+ when 'file'
86
+ if File.file?(item_path)
87
+ Solara.logger.debug("File exists: #{item_path}")
88
+ true
89
+ else
90
+ Solara.logger.failure("File does not exist: #{item_path}")
91
+ false
92
+ end
93
+ when 'folder'
94
+ if File.directory?(item_path)
95
+ true
96
+ else
97
+ Solara.logger.failure("Folder does not exist: #{item_path}")
98
+ false
99
+ end
100
+ else
101
+ Solara.logger.failure("Invalid item type: #{item_type}")
102
+ false
103
+ end
104
+ end
105
+
106
+ def get_path_from_user(item_key, item_type)
107
+ print "Enter the relative path for #{item_key}): "
108
+ STDIN.gets.chomp
109
+ end
110
+ end
@@ -0,0 +1,24 @@
1
+ class AndroidManifestSwitcher
2
+ def initialize
3
+ end
4
+
5
+ def update_manifest(config)
6
+ Solara.logger.start_step("Update AndroidManifest")
7
+ manifest_file = FilePath.android_manifest
8
+ if File.exist?(manifest_file)
9
+ manifest_content = File.read(manifest_file)
10
+ updated_manifest = update_app_name(manifest_content, config)
11
+ File.write(manifest_file, updated_manifest)
12
+ Solara.logger.debug("Updated #{FilePath.android_manifest} to use string resource for app name")
13
+ else
14
+ Solara.logger.debug("❌ #{FilePath.android_manifest} not found. Skipping manifest update.")
15
+ end
16
+ Solara.logger.end_step("Update AndroidManifest")
17
+ end
18
+
19
+ private
20
+
21
+ def update_app_name(manifest_content, config)
22
+ manifest_content.gsub(/android:label="[^"]+"/, 'android:label="@string/app_name"')
23
+ end
24
+ end
@@ -0,0 +1,39 @@
1
+ class AndroidStringsSwitcher
2
+ def initialize
3
+ end
4
+
5
+ def update(config)
6
+ Solara.logger.start_step("Generate artifacts/strings.xml")
7
+ strings_file = FilePath.android_artifacts_strings
8
+
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
13
+
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']}\"")
17
+
18
+ remove_app_name_from_strings
19
+ Solara.logger.end_step("Generate artifacts/strings.xml")
20
+ end
21
+
22
+ # It's important to delete app_name to avoid duplicate resources
23
+ def remove_app_name_from_strings
24
+ file_path = FilePath.android_strings
25
+ manager = StringsXmlManager.new(file_path)
26
+ manager.delete_app_name
27
+ end
28
+
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
+ end
@@ -0,0 +1,233 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+
4
+ class GradleSwitcher
5
+ KOTLIN_IMPORTS = <<-KOTLIN
6
+ import java.io.FileInputStream
7
+ import java.util.Properties
8
+
9
+ KOTLIN
10
+
11
+ KOTLIN_PROPERTIES_LOADER = <<-KOTLIN
12
+ val brandProperties = Properties().apply {
13
+ load(FileInputStream(file("../artifacts/brand.properties")))
14
+ }
15
+ KOTLIN
16
+
17
+ GROOVY_PROPERTIES_LOADER = <<-GROOVY
18
+ project.ext {
19
+ brandProperties = new Properties()
20
+ brandProperties.load(new FileInputStream(file("../artifacts/brand.properties")))
21
+ }
22
+ GROOVY
23
+
24
+ KOTLIN_APPLICATION_ID = 'applicationId = brandProperties.getProperty("applicationId")'
25
+ GROOVY_APPLICATION_ID = "applicationId = project.ext.brandProperties.getProperty('applicationId')"
26
+
27
+ KOTLIN_VERSION_NAME = 'versionName = brandProperties.getProperty("versionName")'
28
+ GROOVY_VERSION_NAME = "versionName = project.ext.brandProperties.getProperty('versionName')"
29
+
30
+ KOTLIN_VERSION_CODE = 'versionCode = brandProperties.getProperty("versionCode").toInt()'
31
+ GROOVY_VERSION_CODE = "versionCode = project.ext.brandProperties.getProperty('versionCode').toInteger()"
32
+
33
+ DEFAULT_SOURCE_SETS = %w[src/main/res src/main/artifacts]
34
+
35
+ def initialize(brand_key)
36
+ @brand_key = brand_key
37
+ @is_kotlin_gradle = FilePath.is_koltin_gradle
38
+ @brand_config = JSON.parse(File.read(FilePath.android_config(brand_key)))
39
+ @source_sets = (@brand_config['sourceSets'] || []).concat(DEFAULT_SOURCE_SETS).uniq
40
+ end
41
+
42
+ def update_build_gradle
43
+ Solara.logger.start_step("Update app/build.gradle")
44
+ gradle_file = FilePath.android_app_gradle
45
+ gradle_content = File.read(gradle_file)
46
+
47
+ update_gradle(gradle_file, gradle_content)
48
+ add_source_sets(gradle_file)
49
+ update_keystore_config(gradle_file)
50
+ Solara.logger.end_step("Update app/build.gradle")
51
+ end
52
+
53
+ private
54
+
55
+ def update_gradle(gradle_file, gradle_content)
56
+ properties_loader = @is_kotlin_gradle ? KOTLIN_PROPERTIES_LOADER : GROOVY_PROPERTIES_LOADER
57
+
58
+ if @is_kotlin_gradle
59
+ # Add imports for Kotlin
60
+ unless gradle_content.include?("import java.io.FileInputStream")
61
+ insert_position = gradle_content.index(/\s*(plugins|android)\s*{/)
62
+ if insert_position.nil?
63
+ raise "Could not find a suitable position to insert imports in #{FilePath.gradle_name}"
64
+ end
65
+ gradle_content.insert(insert_position, KOTLIN_IMPORTS)
66
+ end
67
+ end
68
+
69
+ insert_position = gradle_content.index(/\s*android\s*{/)
70
+ if insert_position.nil?
71
+ raise "Could not find android block in #{FilePath.gradle_name}"
72
+ end
73
+
74
+ unless gradle_content.include?(@is_kotlin_gradle ? "val brandProperties" : "brandProperties = new Properties")
75
+ gradle_content.insert(insert_position + 1, properties_loader)
76
+ end
77
+
78
+ android_block_regex = /(android\s*\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})/m
79
+ updated_android_block = gradle_content.match(android_block_regex)[1]
80
+ .gsub(
81
+ /applicationId\s+=\s+.*/,
82
+ @is_kotlin_gradle ? KOTLIN_APPLICATION_ID : GROOVY_APPLICATION_ID
83
+ )
84
+ .gsub(
85
+ /versionName\s+=\s+.*/,
86
+ @is_kotlin_gradle ? KOTLIN_VERSION_NAME : GROOVY_VERSION_NAME
87
+ ).gsub(
88
+ /versionCode\s+=\s+.*/,
89
+ @is_kotlin_gradle ? KOTLIN_VERSION_CODE : GROOVY_VERSION_CODE
90
+ )
91
+
92
+ gradle_content.sub!(android_block_regex, updated_android_block)
93
+ File.write(gradle_file, gradle_content)
94
+ Solara.logger.debug("Updated #{gradle_file} (#{@is_kotlin_gradle ? 'Kotlin' : 'Groovy'}) to use brand.properties")
95
+ end
96
+
97
+ def add_source_sets(gradle_file)
98
+ content = File.read(gradle_file)
99
+
100
+ source_sets_string = @source_sets.map { |dir| "\"#{dir}\"" }.join(', ')
101
+ kotlin_pattern = /(sourceSets\s*\{\s*getByName\s*\(\s*"main"\s*\)\s*\{\s*res\s*\.\s*srcDirs\s*\(.*?\)\s*\}\s*\})/m
102
+ groovy_pattern = /(sourceSets\s*\{\s*main\s*\{\s*res\s*\.\s*srcDirs\s*=.*?\s*\}\s*\})/m
103
+
104
+ new_config = generate_source_sets(source_sets_string)
105
+
106
+ modified_content = if @is_kotlin_gradle
107
+ if content.match?(kotlin_pattern)
108
+ content.gsub(kotlin_pattern) do |match|
109
+ indent = match[/^\s*/]
110
+ "#{indent}#{new_config.strip}"
111
+ end
112
+ else
113
+ content.sub(/(\s*android\s*\{)/) { "#{$1}\n #{new_config.strip}" }
114
+ end
115
+ else
116
+ if content.match?(groovy_pattern)
117
+ content.gsub(groovy_pattern) do |match|
118
+ indent = match[/^\s*/]
119
+ "#{indent}#{new_config.strip}"
120
+ end
121
+ else
122
+ content.sub(/(\s*android\s*\{)/) { "#{$1}\n #{new_config.strip}" }
123
+ end
124
+ end
125
+
126
+ File.write(gradle_file, modified_content)
127
+ Solara.logger.debug("Source sets configuration updated successfully.")
128
+ end
129
+
130
+ def generate_source_sets(source_sets_string)
131
+ if @is_kotlin_gradle
132
+ <<-KOTLIN
133
+ sourceSets {
134
+ getByName("main") {
135
+ res.srcDirs(listOf(#{source_sets_string}))
136
+ }
137
+ }
138
+ KOTLIN
139
+ else
140
+ <<-GROOVY
141
+ sourceSets {
142
+ main {
143
+ res.srcDirs = [#{source_sets_string}]
144
+ }
145
+ }
146
+ GROOVY
147
+ end
148
+ end
149
+
150
+ def update_keystore_config(gradle_file)
151
+ # We need to apply code signing only if the user has provided its config
152
+ path = FilePath.brand_signing(@brand_key, Platform::Android)
153
+ signing = JSON.parse(File.read(path))
154
+ if signing['storeFile'].empty?
155
+ return
156
+ end
157
+
158
+ content = File.read(gradle_file)
159
+
160
+ # Check if the configuration is already applied
161
+ if content.include?('brandProperties.getProperty("keystore.storeFile")') ||
162
+ content.include?('project.ext.brandProperties.getProperty("keystore.storeFile")')
163
+ Solara.logger.debug("Keystore configuration already applied. Skipping update.")
164
+ return
165
+ end
166
+
167
+ new_config = generate_keystore_config
168
+
169
+ signing_config_pattern = /(signingConfigs\s*\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})/m
170
+ build_types_pattern = /(buildTypes\s*\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})/m
171
+
172
+ modified_content = content.dup
173
+
174
+ # Remove existing buildTypes block if it exists
175
+ modified_content.gsub!(build_types_pattern, '')
176
+
177
+ # Update or add signingConfigs and buildTypes
178
+ if modified_content.match?(signing_config_pattern)
179
+ modified_content.gsub!(signing_config_pattern, new_config)
180
+ else
181
+ modified_content.sub!(/(\s*android\s*\{)/) { "#{$1}\n #{new_config.strip}" }
182
+ end
183
+
184
+ if content != modified_content
185
+ File.write(gradle_file, modified_content)
186
+ Solara.logger.debug("Keystore configuration updated successfully.")
187
+ else
188
+ Solara.logger.debug("No changes were necessary for keystore configuration.")
189
+ end
190
+ end
191
+
192
+ def generate_keystore_config
193
+ if @is_kotlin_gradle
194
+ <<-KOTLIN
195
+ signingConfigs {
196
+ create("release") {
197
+ storeFile = file(brandProperties.getProperty("storeFile"))
198
+ storePassword = brandProperties.getProperty("storePassword")
199
+ keyAlias = brandProperties.getProperty("keyAlias")
200
+ keyPassword = brandProperties.getProperty("keyPassword")
201
+ }
202
+ }
203
+ buildTypes {
204
+ getByName("release") {
205
+ signingConfig = signingConfigs.getByName("release")
206
+ }
207
+ getByName("debug") {
208
+ isDebuggable = true
209
+ }
210
+ }
211
+ KOTLIN
212
+ else
213
+ <<-GROOVY
214
+ signingConfigs {
215
+ release {
216
+ storeFile file(project.ext.brandProperties.getProperty("storeFile"))
217
+ storePassword project.ext.brandProperties.getProperty("storePassword")
218
+ keyAlias project.ext.brandProperties.getProperty("keyAlias")
219
+ keyPassword project.ext.brandProperties.getProperty("keyPassword")
220
+ }
221
+ }
222
+ buildTypes {
223
+ release {
224
+ signingConfig signingConfigs.release
225
+ }
226
+ debug {
227
+ debuggable true
228
+ }
229
+ }
230
+ GROOVY
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,31 @@
1
+ class PropertiesGenerator
2
+ def initialize(brand_key)
3
+ @brand_key = brand_key
4
+ end
5
+
6
+ def generate
7
+ Solara.logger.start_step("Generate brand.properties for Android")
8
+ output_file = FilePath.android_generated_properties
9
+ content = "# Generated by Solara\n"
10
+
11
+ load_config.each do |key, value|
12
+ content << "#{key}=#{value}\n"
13
+ end
14
+
15
+ File.write(output_file, content)
16
+ Solara.logger.debug("🎉 Generated properties file: #{output_file}, content below ⬇️")
17
+ Solara.logger.debug("--------------\n#{content}--------------")
18
+ Solara.logger.end_step("Generate brand.properties for Android")
19
+ end
20
+
21
+ def load_config
22
+ config_path = FilePath.android_brand_config(@brand_key)
23
+ signing_path = FilePath.brand_signing(@brand_key, Platform::Android)
24
+ config = JSON.parse(File.read(config_path))
25
+ signing_config = JSON.parse(File.read(signing_path))
26
+
27
+ config.merge!(signing_config)
28
+
29
+ config
30
+ end
31
+ end
@@ -0,0 +1,109 @@
1
+ Dir.glob("#{__dir__}/*.rb").each { |file| require file }
2
+ require 'singleton'
3
+ require 'pathname'
4
+
5
+ class IOSFilePathManager
6
+ include Singleton
7
+
8
+ def initialize
9
+ @platform = SolaraSettingsManager.instance.platform
10
+ @project_root = SolaraSettingsManager.instance.project_root
11
+ end
12
+
13
+ def xcode_project
14
+ path = ProjectSettingsManager.instance.value('xcodeproj', Platform::IOS)
15
+ File.join(@project_root, path)
16
+ end
17
+
18
+ def xcode_project_directory
19
+ Pathname.new(xcode_project).parent.to_s
20
+ end
21
+
22
+ def brand_assets(brand_key)
23
+ File.join(FilePath.brands, brand_key, FilePath.ios, 'assets')
24
+ end
25
+
26
+ def brand_xcconfig
27
+ File.join(artifacts, 'Brand.xcconfig')
28
+ end
29
+
30
+ def artifacts
31
+ case @platform
32
+ when Platform::Flutter
33
+ return File.join(@project_root, FilePath.ios, 'Flutter', 'Artifacts')
34
+ when Platform::IOS
35
+ return File.join(xcode_project_directory, 'Artifacts')
36
+ else
37
+ raise ArgumentError, "Invalid platform: #{@platform}"
38
+ end
39
+ end
40
+
41
+ def info_plist
42
+ path = ProjectSettingsManager.instance.value('Info.plist', Platform::IOS)
43
+ File.join(@project_root, path)
44
+ end
45
+
46
+ def assets
47
+ path = ProjectSettingsManager.instance.value('Assets.xcassets', Platform::IOS)
48
+ File.join(@project_root, path)
49
+ end
50
+
51
+ def assets_directory
52
+ File.dirname(assets)
53
+ end
54
+
55
+ def assets_artifacts
56
+ File.join(assets_directory, 'Assets.xcassets', 'Artifacts')
57
+ end
58
+
59
+ def assets_artifcats
60
+ File.join(assets, 'Artifacts')
61
+ end
62
+
63
+ def app_xcconfig(name)
64
+ case @platform
65
+ when Platform::Flutter
66
+ return File.join(@project_root, FilePath.ios, 'Flutter', name)
67
+ when Platform::IOS
68
+ return File.join(xcode_project_directory, 'XCConfig', name)
69
+ else
70
+ raise ArgumentError, "Invalid platform: #{@platform}"
71
+ end
72
+ end
73
+
74
+ def app_xcconfig_directory
75
+ Pathname.new(app_xcconfig('Debug.xcconfig')).parent.to_s
76
+ end
77
+
78
+ def brand_app_icon(brand_key)
79
+ File.join(FilePath.brands, brand_key, FilePath.ios, 'assets', 'AppIcon.appiconset')
80
+ end
81
+
82
+ def brand_app_icon_image(brand_key)
83
+ appicon_set_path = brand_app_icon(brand_key)
84
+
85
+ if appicon_set_path.nil?
86
+ raise "Error: AppIcon.appiconset not found for brand #{brand_key}"
87
+ end
88
+
89
+ contents_json_path = File.join(appicon_set_path, 'Contents.json')
90
+
91
+ unless File.exist?(contents_json_path)
92
+ raise "Error: Contents.json not found in AppIcon.appiconset for brand #{brand_key}"
93
+ end
94
+
95
+ contents = JSON.parse(File.read(contents_json_path))
96
+
97
+ largest_image = contents['images'].max_by do |img|
98
+ size = img['size'].scan(/(\d+)x(\d+)/).first&.map(&:to_i)
99
+ size ? size[0] * size[1] : 0
100
+ end
101
+
102
+ if largest_image
103
+ File.join(appicon_set_path, largest_image['filename'])
104
+ else
105
+ raise "No images found in Contents.json for brand #{brand_key}"
106
+ end
107
+ end
108
+
109
+ end
@@ -0,0 +1,42 @@
1
+ require 'xcodeproj'
2
+
3
+ class IOSPlistManager
4
+ def initialize(project, info_plist_path)
5
+ @project = project
6
+ @info_plist_path = info_plist_path
7
+ end
8
+
9
+ def create_and_add_info_plist
10
+ # add_info_plist_to_project
11
+ set_info_plist_in_build_settings
12
+ save_project
13
+ end
14
+
15
+ private
16
+
17
+ def add_info_plist_to_project
18
+ file_ref = @project.files.select { |f| f.path == @info_plist_path }.first
19
+ if file_ref
20
+ Solara.logger.debug("Info.plist file reference already exists in the project. Skipping this step.")
21
+ else
22
+ file_ref = XcodeProjectManager.new.add_single_file_to_group(@project, @project.main_group, @info_plist_path)
23
+ Solara.logger.debug("Info.plist file created. file_ref = #{file_ref}")
24
+ end
25
+ end
26
+
27
+ def set_info_plist_in_build_settings
28
+ path = FileManager.get_relative_path(IOSFilePathManager.instance.xcode_project_directory, @info_plist_path)
29
+ main_target.build_configurations.each do |config|
30
+ config.build_settings['INFOPLIST_FILE'] = path
31
+ end
32
+ end
33
+
34
+ def save_project
35
+ @project.save
36
+ end
37
+
38
+ def main_target
39
+ @project.targets.first
40
+ end
41
+
42
+ end
@@ -0,0 +1,44 @@
1
+ class XcconfigGenerator
2
+ def initialize(brand_key)
3
+ @brand_key = brand_key
4
+ @platform = SolaraSettingsManager.instance.platform
5
+ end
6
+
7
+ def generate
8
+ Solara.logger.start_step("Generate Brand.xcconfig for iOS")
9
+ destination = IOSFilePathManager.instance.brand_xcconfig
10
+ content = generate_xcconfig_content
11
+ File.write(destination, content)
12
+ Solara.logger.debug("🎉 Generated #{IOSFilePathManager.instance.brand_xcconfig}. Content below ⬇️")
13
+ Solara.logger.debug("--------------\n#{content}--------------")
14
+ Solara.logger.end_step("Generate Brand.xcconfig for iOS")
15
+ end
16
+
17
+ private
18
+
19
+ def generate_xcconfig_content
20
+ content = <<~XCCONFIG
21
+ ASSETCATALOG_COMPILER_APPICON_NAME = Artifacts/AppIcon
22
+ #{load_config.map { |key, value| "#{key.upcase} = #{value}" }.join("\n")}
23
+ XCCONFIG
24
+
25
+ case @platform
26
+ when Platform::Flutter
27
+ # "../Generated.xcconfig" is the config related to Flutter itself, we must include it here.
28
+ <<~XCCONFIG
29
+ #include "../Generated.xcconfig"
30
+
31
+ #{content}
32
+ XCCONFIG
33
+ when Platform::IOS
34
+ content
35
+ else
36
+ raise ArgumentError, "Invalid platform: #{@platform}"
37
+ end
38
+ end
39
+
40
+ def load_config
41
+ config_path = FilePath.ios_config(@brand_key)
42
+ JSON.parse(File.read(config_path))
43
+ end
44
+ end
@@ -0,0 +1,56 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+
4
+ class XcodeAssetManager
5
+ def initialize(asset_catalog_path)
6
+ @asset_catalog_path = asset_catalog_path
7
+ end
8
+
9
+
10
+ def add(source)
11
+ # Fetches only files in the specified source directory
12
+ Dir.glob(File.join(source, '*.{png,jpg,jpeg,svg,heic,pdf}')).each do |file_path|
13
+ if File.file?(file_path) # Ensure it's a file and not a directory
14
+ filename_without_extension = File.basename(file_path, File.extname(file_path))
15
+ add_image(filename_without_extension, file_path, '1x')
16
+ end
17
+ end
18
+ end
19
+
20
+ def add_image(image_name, image_path, scale = '1x')
21
+ # Create the image set directory if it doesn't exist
22
+ image_set_path = File.join(@asset_catalog_path, "#{image_name}.imageset")
23
+ FileUtils.mkdir_p(image_set_path)
24
+
25
+ # Copy the image file to the image set directory
26
+ destination_path = File.join(image_set_path, "#{image_name}@#{scale}.png")
27
+ FileUtils.cp(image_path, destination_path)
28
+
29
+ # Update or create the Contents.json file
30
+ contents_json_path = File.join(image_set_path, 'Contents.json')
31
+ contents = if File.exist?(contents_json_path)
32
+ JSON.parse(File.read(contents_json_path))
33
+ else
34
+ { "images" => [], "info" => { "version" => 1, "author" => "solara" } }
35
+ end
36
+
37
+ # Add or update the image entry
38
+ image_entry = {
39
+ "idiom" => "universal",
40
+ "filename" => "#{image_name}@#{scale}.png",
41
+ "scale" => scale
42
+ }
43
+
44
+ existing_entry = contents["images"].find { |img| img["scale"] == scale }
45
+ if existing_entry
46
+ existing_entry.merge!(image_entry)
47
+ else
48
+ contents["images"] << image_entry
49
+ end
50
+
51
+ # Write the updated Contents.json
52
+ File.write(contents_json_path, JSON.pretty_generate(contents))
53
+
54
+ puts "Image '#{image_name}' added successfully at scale #{scale}."
55
+ end
56
+ end