solara 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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