solara 0.4.0 → 0.6.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.
- checksums.yaml +4 -4
 - data/solara/lib/.DS_Store +0 -0
 - data/solara/lib/core/.DS_Store +0 -0
 - data/solara/lib/core/brands/brand_onboarder.rb +1 -1
 - data/solara/lib/core/brands/brand_switcher.rb +92 -1
 - data/solara/lib/core/dashboard/brand/BrandDetail.js +34 -2
 - data/solara/lib/core/dashboard/brand/BrandDetailController.js +27 -234
 - data/solara/lib/core/dashboard/brand/BrandDetailModel.js +14 -5
 - data/solara/lib/core/dashboard/brand/BrandDetailView.js +16 -200
 - data/solara/lib/core/dashboard/brand/SectionsFormManager.js +293 -0
 - data/solara/lib/core/dashboard/brand/brand.html +223 -174
 - data/solara/lib/core/dashboard/brand/source/BrandLocalSource.js +2 -5
 - data/solara/lib/core/dashboard/brand/source/BrandRemoteSource.js +36 -133
 - data/solara/lib/core/dashboard/brands/Brands.js +31 -0
 - data/solara/lib/core/dashboard/brands/BrandsController.js +0 -5
 - data/solara/lib/core/dashboard/brands/BrandsView.js +2 -2
 - data/solara/lib/core/dashboard/brands/brands.html +71 -52
 - data/solara/lib/core/dashboard/component/AliasesBottomSheet.js +6 -6
 - data/solara/lib/core/dashboard/component/BrandOptionsBottomSheet.js +4 -4
 - data/solara/lib/core/dashboard/component/ConfirmationDialog.js +15 -10
 - data/solara/lib/core/dashboard/component/EditJsonSheet.js +160 -0
 - data/solara/lib/core/dashboard/component/MessageBottomSheet.js +5 -5
 - data/solara/lib/core/dashboard/component/OnboardBrandBottomSheet.js +9 -3
 - data/solara/lib/core/dashboard/handler/base_handler.rb +1 -0
 - data/solara/lib/core/dashboard/handler/edit_section_handler.rb +1 -5
 - data/solara/lib/core/dashboard/handler/onboard_brand_handler.rb +0 -15
 - data/solara/lib/core/doctor/schema/brand_configurations.json +0 -8
 - data/solara/lib/core/doctor/schema/platform/global/resources_manifest.json +30 -0
 - data/solara/lib/core/doctor/schema/platform/json_manifest.json +39 -0
 - data/solara/lib/core/doctor/validator/template/android_template_validation_config.yml +35 -1
 - data/solara/lib/core/doctor/validator/template/flutter_template_validation_config.yml +30 -1
 - data/solara/lib/core/doctor/validator/template/ios_template_validation_config.yml +35 -1
 - data/solara/lib/core/doctor/validator/template/template_validator.rb +9 -9
 - data/solara/lib/core/scripts/brand_config_manager.rb +1 -1
 - data/solara/lib/core/scripts/brand_configurations_manager.rb +41 -0
 - data/solara/lib/core/scripts/code_generator.rb +342 -118
 - data/solara/lib/core/scripts/file_manager.rb +11 -15
 - data/solara/lib/core/scripts/file_path.rb +21 -1
 - data/solara/lib/core/scripts/gitignore_manager.rb +12 -6
 - data/solara/lib/core/scripts/json_manifest_processor.rb +136 -0
 - data/solara/lib/core/scripts/platform/ios/infoplist_string_catalog_manager.rb +11 -1
 - data/solara/lib/core/scripts/resource_manifest_processor.rb +151 -0
 - data/solara/lib/core/scripts/solara_status_manager.rb +1 -1
 - data/solara/lib/core/scripts/theme_generator.rb +21 -242
 - data/solara/lib/core/solara_configurator.rb +1 -1
 - data/solara/lib/core/template/brands/global/resources_manifest.json +10 -0
 - data/solara/lib/core/template/brands/json/Json-Manifest.md +59 -0
 - data/solara/lib/core/template/brands/json/json_manifest.json +16 -0
 - data/solara/lib/core/template/brands/shared/theme.json +213 -29
 - data/solara/lib/core/template/config/android_template_config.json +50 -0
 - data/solara/lib/core/template/config/flutter_template_config.json +35 -0
 - data/solara/lib/core/template/config/ios_template_config.json +50 -0
 - data/solara/lib/core/template/configurations.json +46 -0
 - data/solara/lib/core/template/project_template_generator.rb +2 -0
 - data/solara/lib/solara/version.rb +1 -1
 - data/solara/lib/solara.rb +19 -0
 - data/solara/lib/solara_manager.rb +21 -13
 - metadata +13 -4
 - data/solara/lib/core/dashboard/component/AddFieldSheet.js +0 -175
 - data/solara/lib/core/dashboard/handler/brand_configurations_manager.rb +0 -73
 
| 
         @@ -5,23 +5,19 @@ class FileManager 
     | 
|
| 
       5 
5 
     | 
    
         
             
                    source_path = Pathname.new(source_dir).expand_path
         
     | 
| 
       6 
6 
     | 
    
         
             
                    destination_path = Pathname.new(destination_dir).expand_path
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
     | 
    
         
            -
                     
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
                        destination_item_path = destination_path.join(relative_path)
         
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
                        if File.directory?(item)
         
     | 
| 
       14 
     | 
    
         
            -
                          FileUtils.mkdir_p(destination_item_path)
         
     | 
| 
       15 
     | 
    
         
            -
                          FileUtils.cp_r(item + '/.', destination_item_path) # Ensure to copy contents
         
     | 
| 
       16 
     | 
    
         
            -
                        else
         
     | 
| 
       17 
     | 
    
         
            -
                          FileUtils.mkdir_p(destination_item_path.dirname) # Create parent directory
         
     | 
| 
       18 
     | 
    
         
            -
                          FileUtils.cp(item, destination_item_path)
         
     | 
| 
       19 
     | 
    
         
            -
                        end
         
     | 
| 
      
 8 
     | 
    
         
            +
                    Dir.glob(source_path.join('*')).each do |item|
         
     | 
| 
      
 9 
     | 
    
         
            +
                    relative_path = Pathname.new(item).relative_path_from(source_path).to_s
         
     | 
| 
      
 10 
     | 
    
         
            +
                    destination_item_path = destination_path.join(relative_path)
         
     | 
| 
       20 
11 
     | 
    
         | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
      
 12 
     | 
    
         
            +
                    if File.directory?(item)
         
     | 
| 
      
 13 
     | 
    
         
            +
                        FileUtils.mkdir_p(destination_item_path)
         
     | 
| 
      
 14 
     | 
    
         
            +
                        FileUtils.cp_r(item + '/.', destination_item_path) # Ensure to copy contents
         
     | 
| 
       23 
15 
     | 
    
         
             
                    else
         
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
      
 16 
     | 
    
         
            +
                        FileUtils.mkdir_p(destination_item_path.dirname) # Create parent directory
         
     | 
| 
      
 17 
     | 
    
         
            +
                        FileUtils.cp(item, destination_item_path)
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    Solara.logger.debug("🚗 Copied #{relative_path} \n\t↑ From: #{source_path} \n\t↓ To:   #{destination_path}")
         
     | 
| 
       25 
21 
     | 
    
         
             
                    end
         
     | 
| 
       26 
22 
     | 
    
         
             
                  end
         
     | 
| 
       27 
23 
     | 
    
         | 
| 
         @@ -99,6 +99,14 @@ module FilePath 
     | 
|
| 
       99 
99 
     | 
    
         
             
                    File.join(brands, brand_key)
         
     | 
| 
       100 
100 
     | 
    
         
             
                end
         
     | 
| 
       101 
101 
     | 
    
         | 
| 
      
 102 
     | 
    
         
            +
                def self.brand_json_dir(brand_key, platform = nil)
         
     | 
| 
      
 103 
     | 
    
         
            +
                    File.join(brand(brand_key), platform || SolaraSettingsManager.instance.platform, 'json')
         
     | 
| 
      
 104 
     | 
    
         
            +
                end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                def self.brand_global_json_dir
         
     | 
| 
      
 107 
     | 
    
         
            +
                    File.join(global, 'json')
         
     | 
| 
      
 108 
     | 
    
         
            +
                end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
       102 
110 
     | 
    
         
             
                def self.android_config(brand_key)
         
     | 
| 
       103 
111 
     | 
    
         
             
                    File.join(android_brand_root(brand_key), 'android_config.json')
         
     | 
| 
       104 
112 
     | 
    
         
             
                end
         
     | 
| 
         @@ -116,7 +124,15 @@ module FilePath 
     | 
|
| 
       116 
124 
     | 
    
         
             
                end
         
     | 
| 
       117 
125 
     | 
    
         | 
| 
       118 
126 
     | 
    
         
             
                def self.brand_fonts
         
     | 
| 
       119 
     | 
    
         
            -
                    File.join( 
     | 
| 
      
 127 
     | 
    
         
            +
                    File.join(global, 'fonts')
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                def self.resources_manifest
         
     | 
| 
      
 131 
     | 
    
         
            +
                    File.join(global, 'resources_manifest.json')
         
     | 
| 
      
 132 
     | 
    
         
            +
                end
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                def self.global
         
     | 
| 
      
 135 
     | 
    
         
            +
                    File.join(solara_brand, 'global')
         
     | 
| 
       120 
136 
     | 
    
         
             
                end
         
     | 
| 
       121 
137 
     | 
    
         | 
| 
       122 
138 
     | 
    
         
             
                def self.brand_config(brand_key)
         
     | 
| 
         @@ -287,6 +303,10 @@ module FilePath 
     | 
|
| 
       287 
303 
     | 
    
         
             
                    File.join(root, 'core', 'template')
         
     | 
| 
       288 
304 
     | 
    
         
             
                end
         
     | 
| 
       289 
305 
     | 
    
         | 
| 
      
 306 
     | 
    
         
            +
                def self.brand_configurations
         
     | 
| 
      
 307 
     | 
    
         
            +
                    File.join(solara_template, 'configurations.json')
         
     | 
| 
      
 308 
     | 
    
         
            +
                end
         
     | 
| 
      
 309 
     | 
    
         
            +
             
     | 
| 
       290 
310 
     | 
    
         
             
                def self.solara_aliases_json
         
     | 
| 
       291 
311 
     | 
    
         
             
                    File.join(dot_solara, 'aliases', 'aliases.json')
         
     | 
| 
       292 
312 
     | 
    
         
             
                end
         
     | 
| 
         @@ -4,7 +4,7 @@ class GitignoreManager 
     | 
|
| 
       4 
4 
     | 
    
         
             
                    create_gitignore_if_not_exists
         
     | 
| 
       5 
5 
     | 
    
         
             
                end
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
                def self. 
     | 
| 
      
 7 
     | 
    
         
            +
                def self.ignore_common_files
         
     | 
| 
       8 
8 
     | 
    
         
             
                    Solara.logger.start_step("Exclude Brand-Generated Files and Folders from Git")
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
                    items = [
         
     | 
| 
         @@ -18,9 +18,7 @@ class GitignoreManager 
     | 
|
| 
       18 
18 
     | 
    
         
             
                     ]
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
       20 
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'
         
     | 
| 
      
 21 
     | 
    
         
            +
                        items << "/#{FileManager.get_relative_path_to_root(FilePath.project_infoplist_string_catalog)}"
         
     | 
| 
       24 
22 
     | 
    
         
             
                    end
         
     | 
| 
       25 
23 
     | 
    
         | 
| 
       26 
24 
     | 
    
         
             
                    GitignoreManager.new(FilePath.project_root).add_items(items)
         
     | 
| 
         @@ -29,7 +27,7 @@ class GitignoreManager 
     | 
|
| 
       29 
27 
     | 
    
         | 
| 
       30 
28 
     | 
    
         
             
                def add_items(items)
         
     | 
| 
       31 
29 
     | 
    
         
             
                    items.each do |item|
         
     | 
| 
       32 
     | 
    
         
            -
                        add_item(item)
         
     | 
| 
      
 30 
     | 
    
         
            +
                        add_item(FileManager.get_relative_path_to_root(item))
         
     | 
| 
       33 
31 
     | 
    
         
             
                    end
         
     | 
| 
       34 
32 
     | 
    
         
             
                end
         
     | 
| 
       35 
33 
     | 
    
         | 
| 
         @@ -39,7 +37,15 @@ class GitignoreManager 
     | 
|
| 
       39 
37 
     | 
    
         
             
                    if existing_items.include?(item)
         
     | 
| 
       40 
38 
     | 
    
         
             
                        Solara.logger.debug("'#{item}' already exists in .gitignore")
         
     | 
| 
       41 
39 
     | 
    
         
             
                    else
         
     | 
| 
       42 
     | 
    
         
            -
                        File.open(@gitignore_path, 'a') do |file|
         
     | 
| 
      
 40 
     | 
    
         
            +
                        File.open(@gitignore_path, 'a+') do |file|
         
     | 
| 
      
 41 
     | 
    
         
            +
                            # Move the file pointer to the beginning to check the last character
         
     | 
| 
      
 42 
     | 
    
         
            +
                            file.seek(0, IO::SEEK_END)
         
     | 
| 
      
 43 
     | 
    
         
            +
                            if file.size > 0
         
     | 
| 
      
 44 
     | 
    
         
            +
                                # Only add a new line if the last character is not a newline
         
     | 
| 
      
 45 
     | 
    
         
            +
                                file.seek(-1, IO::SEEK_END)
         
     | 
| 
      
 46 
     | 
    
         
            +
                                last_char = file.getc
         
     | 
| 
      
 47 
     | 
    
         
            +
                                file.puts if last_char != "\n"
         
     | 
| 
      
 48 
     | 
    
         
            +
                            end
         
     | 
| 
       43 
49 
     | 
    
         
             
                            file.puts(item)
         
     | 
| 
       44 
50 
     | 
    
         
             
                        end
         
     | 
| 
       45 
51 
     | 
    
         
             
                        Solara.logger.debug("Added '#{item}' to .gitignore")
         
     | 
| 
         @@ -0,0 +1,136 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class JsonManifestProcessor
         
     | 
| 
      
 4 
     | 
    
         
            +
              def initialize(json_path, language, output_path)
         
     | 
| 
      
 5 
     | 
    
         
            +
                @json_path = json_path
         
     | 
| 
      
 6 
     | 
    
         
            +
                @language = language
         
     | 
| 
      
 7 
     | 
    
         
            +
                @output_path = output_path
         
     | 
| 
      
 8 
     | 
    
         
            +
                @manifest = read_manifest
         
     | 
| 
      
 9 
     | 
    
         
            +
              end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              def process
         
     | 
| 
      
 12 
     | 
    
         
            +
                # First process files specified in manifest
         
     | 
| 
      
 13 
     | 
    
         
            +
                process_manifest_files if @manifest && @manifest['files']
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                # Then process remaining JSON files
         
     | 
| 
      
 16 
     | 
    
         
            +
                process_remaining_files
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              private
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              def read_manifest
         
     | 
| 
      
 22 
     | 
    
         
            +
                manifest_path = File.join(@json_path, 'json_manifest.json')
         
     | 
| 
      
 23 
     | 
    
         
            +
                JSON.parse(File.read(manifest_path))
         
     | 
| 
      
 24 
     | 
    
         
            +
              rescue JSON::ParserError => e
         
     | 
| 
      
 25 
     | 
    
         
            +
                Solara.logger.debug("Error parsing manifest JSON: #{e.message}")
         
     | 
| 
      
 26 
     | 
    
         
            +
                nil
         
     | 
| 
      
 27 
     | 
    
         
            +
              rescue Errno::ENOENT => e
         
     | 
| 
      
 28 
     | 
    
         
            +
                Solara.logger.debug("Manifest file not found: #{e.message}")
         
     | 
| 
      
 29 
     | 
    
         
            +
                nil
         
     | 
| 
      
 30 
     | 
    
         
            +
              rescue StandardError => e
         
     | 
| 
      
 31 
     | 
    
         
            +
                Solara.logger.debug("Unexpected error reading manifest: #{e.message}")
         
     | 
| 
      
 32 
     | 
    
         
            +
                nil
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              def process_manifest_files
         
     | 
| 
      
 36 
     | 
    
         
            +
                @manifest['files'].each do |file|
         
     | 
| 
      
 37 
     | 
    
         
            +
                  process_manifest_file(file)
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
              def process_manifest_file(file)
         
     | 
| 
      
 42 
     | 
    
         
            +
                return unless file['generate']
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                file_name = file['fileName']
         
     | 
| 
      
 45 
     | 
    
         
            +
                class_name = file['parentClassName']
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                return if file_name.empty? || class_name.empty?
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                custom_class_names = convert_to_map(file['customClassNames'])
         
     | 
| 
      
 50 
     | 
    
         
            +
                process_json_file(
         
     | 
| 
      
 51 
     | 
    
         
            +
                  File.join(@json_path, file_name),
         
     | 
| 
      
 52 
     | 
    
         
            +
                  class_name,
         
     | 
| 
      
 53 
     | 
    
         
            +
                  custom_class_names,
         
     | 
| 
      
 54 
     | 
    
         
            +
                  true
         
     | 
| 
      
 55 
     | 
    
         
            +
                )
         
     | 
| 
      
 56 
     | 
    
         
            +
              end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
              def process_remaining_files
         
     | 
| 
      
 59 
     | 
    
         
            +
                manifest_files = @manifest&.dig('files')&.map { |f| f['fileName'] } || []
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                json_files = get_json_files
         
     | 
| 
      
 62 
     | 
    
         
            +
                json_files.each do |file_path|
         
     | 
| 
      
 63 
     | 
    
         
            +
                  file_name = File.basename(file_path)
         
     | 
| 
      
 64 
     | 
    
         
            +
                  # Skip files that were already processed via manifest
         
     | 
| 
      
 65 
     | 
    
         
            +
                  next if manifest_files.include?(file_name)
         
     | 
| 
      
 66 
     | 
    
         
            +
                  next if file_name == 'json_manifest.json'
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  class_name = derive_class_name(file_name)
         
     | 
| 
      
 69 
     | 
    
         
            +
                  process_json_file(file_path, class_name, {}, false)
         
     | 
| 
      
 70 
     | 
    
         
            +
                end
         
     | 
| 
      
 71 
     | 
    
         
            +
              end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
              def get_json_files
         
     | 
| 
      
 74 
     | 
    
         
            +
                Dir.glob(File.join(@json_path, '**', '*.json'))
         
     | 
| 
      
 75 
     | 
    
         
            +
              rescue StandardError => e
         
     | 
| 
      
 76 
     | 
    
         
            +
                Solara.logger.debug("Error reading directory #{@json_path}: #{e.message}")
         
     | 
| 
      
 77 
     | 
    
         
            +
                []
         
     | 
| 
      
 78 
     | 
    
         
            +
              end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
              def process_json_file(file_path, class_name, custom_types, is_manifest_file)
         
     | 
| 
      
 81 
     | 
    
         
            +
                begin
         
     | 
| 
      
 82 
     | 
    
         
            +
                  json_content = JSON.parse(File.read(file_path))
         
     | 
| 
      
 83 
     | 
    
         
            +
                  code_generator = CodeGenerator.new(
         
     | 
| 
      
 84 
     | 
    
         
            +
                    json: json_content,
         
     | 
| 
      
 85 
     | 
    
         
            +
                    language: @language,
         
     | 
| 
      
 86 
     | 
    
         
            +
                    parent_class_name: class_name,
         
     | 
| 
      
 87 
     | 
    
         
            +
                    custom_types: custom_types
         
     | 
| 
      
 88 
     | 
    
         
            +
                  )
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                  generated_code = code_generator.generate
         
     | 
| 
      
 91 
     | 
    
         
            +
                  output_path = File.join(@output_path, generated_filename(class_name))
         
     | 
| 
      
 92 
     | 
    
         
            +
                  write_to_file(output_path, generated_code)
         
     | 
| 
      
 93 
     | 
    
         
            +
                rescue JSON::ParserError => e
         
     | 
| 
      
 94 
     | 
    
         
            +
                  Solara.logger.debug("Error parsing JSON file #{File.basename(file_path)}: #{e.message}")
         
     | 
| 
      
 95 
     | 
    
         
            +
                rescue StandardError => e
         
     | 
| 
      
 96 
     | 
    
         
            +
                  Solara.logger.debug("Error processing file #{File.basename(file_path)}: #{e.message}")
         
     | 
| 
      
 97 
     | 
    
         
            +
                end
         
     | 
| 
      
 98 
     | 
    
         
            +
              end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
              def derive_class_name(file_name)
         
     | 
| 
      
 101 
     | 
    
         
            +
                # Remove .json extension and convert to PascalCase
         
     | 
| 
      
 102 
     | 
    
         
            +
                base_name = File.basename(file_name, '.json')
         
     | 
| 
      
 103 
     | 
    
         
            +
                base_name.split('_').map(&:capitalize).join
         
     | 
| 
      
 104 
     | 
    
         
            +
              end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
              def convert_to_map(custom_class_names)
         
     | 
| 
      
 107 
     | 
    
         
            +
                return {} unless custom_class_names
         
     | 
| 
      
 108 
     | 
    
         
            +
                custom_class_names.each_with_object({}) do |item, result|
         
     | 
| 
      
 109 
     | 
    
         
            +
                  result[item['originalName']] = item['customName']
         
     | 
| 
      
 110 
     | 
    
         
            +
                end
         
     | 
| 
      
 111 
     | 
    
         
            +
              end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
              def generated_filename(class_name)
         
     | 
| 
      
 114 
     | 
    
         
            +
                case SolaraSettingsManager.instance.platform
         
     | 
| 
      
 115 
     | 
    
         
            +
                when Platform::Flutter
         
     | 
| 
      
 116 
     | 
    
         
            +
                  "#{to_snake_case(class_name)}.dart"
         
     | 
| 
      
 117 
     | 
    
         
            +
                when Platform::IOS
         
     | 
| 
      
 118 
     | 
    
         
            +
                  "#{class_name}.swift"
         
     | 
| 
      
 119 
     | 
    
         
            +
                when Platform::Android
         
     | 
| 
      
 120 
     | 
    
         
            +
                  "#{class_name}.kt"
         
     | 
| 
      
 121 
     | 
    
         
            +
                else
         
     | 
| 
      
 122 
     | 
    
         
            +
                  raise ArgumentError, "Invalid platform: #{@platform}"
         
     | 
| 
      
 123 
     | 
    
         
            +
                end
         
     | 
| 
      
 124 
     | 
    
         
            +
              end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
              def to_snake_case(string)
         
     | 
| 
      
 127 
     | 
    
         
            +
                string.gsub(/[A-Z]/) { |match| "_#{match.downcase}" }.sub(/^_/, '')
         
     | 
| 
      
 128 
     | 
    
         
            +
              end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
              def write_to_file(output, content)
         
     | 
| 
      
 131 
     | 
    
         
            +
                File.write(output, content)
         
     | 
| 
      
 132 
     | 
    
         
            +
                Solara.logger.debug("Generated #{output}")
         
     | 
| 
      
 133 
     | 
    
         
            +
              rescue StandardError => e
         
     | 
| 
      
 134 
     | 
    
         
            +
                Solara.logger.debug("Error writing to file #{output}: #{e.message}")
         
     | 
| 
      
 135 
     | 
    
         
            +
              end
         
     | 
| 
      
 136 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -2,12 +2,22 @@ require 'json' 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            module StringCatalogUtils
         
     | 
| 
       4 
4 
     | 
    
         
             
              def load_string_catalog(path)
         
     | 
| 
      
 5 
     | 
    
         
            +
                @path = path
         
     | 
| 
       5 
6 
     | 
    
         
             
                JSON.parse(File.read(path))
         
     | 
| 
       6 
7 
     | 
    
         
             
              end
         
     | 
| 
       7 
8 
     | 
    
         | 
| 
       8 
9 
     | 
    
         
             
              def get_value(data, key, target, language)
         
     | 
| 
       9 
10 
     | 
    
         
             
                lang = language || data['sourceLanguage']
         
     | 
| 
       10 
     | 
    
         
            -
                data 
     | 
| 
      
 11 
     | 
    
         
            +
                localizations = data.dig('strings', key, 'localizations', lang)
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                unless localizations && localizations['stringUnit']
         
     | 
| 
      
 14 
     | 
    
         
            +
                  error_message = "The default language is #{lang}, but no localizations are available for key '#{key}'. Please address this issue in {@path}. You can easily open the file in Xcode to make the necessary adjustments."
         
     | 
| 
      
 15 
     | 
    
         
            +
                  Solara.logger.fatal(error_message)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  exit 1
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                string_unit = localizations['stringUnit']
         
     | 
| 
      
 20 
     | 
    
         
            +
                string_unit[target]
         
     | 
| 
       11 
21 
     | 
    
         
             
              end
         
     | 
| 
       12 
22 
     | 
    
         | 
| 
       13 
23 
     | 
    
         
             
              def has_value?(data, key, language)
         
     | 
| 
         @@ -0,0 +1,151 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'fileutils'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            class ResourceManifestProcessor
         
     | 
| 
      
 5 
     | 
    
         
            +
              def initialize(brand_key, ignore_health_check:)
         
     | 
| 
      
 6 
     | 
    
         
            +
                @brand_key = brand_key
         
     | 
| 
      
 7 
     | 
    
         
            +
                @ignore_health_check = ignore_health_check
         
     | 
| 
      
 8 
     | 
    
         
            +
                @manifest_file = FilePath.resources_manifest
         
     | 
| 
      
 9 
     | 
    
         
            +
                @config = load_manifest_file
         
     | 
| 
      
 10 
     | 
    
         
            +
              end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              def copy
         
     | 
| 
      
 13 
     | 
    
         
            +
                @base_source_path = FilePath.brands
         
     | 
| 
      
 14 
     | 
    
         
            +
                @base_destination_path = FilePath.project_root
         
     | 
| 
      
 15 
     | 
    
         
            +
                @config['files'].each do |item|
         
     | 
| 
      
 16 
     | 
    
         
            +
                  process_file_item(item)
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
              private
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
              def clean(item, src, dst)
         
     | 
| 
      
 23 
     | 
    
         
            +
                paths = destinations(item, src, dst)
         
     | 
| 
      
 24 
     | 
    
         
            +
                paths.each do |path|
         
     | 
| 
      
 25 
     | 
    
         
            +
                  File.delete(path) if File.exist?(path)
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
              def process_file_item(item)
         
     | 
| 
      
 30 
     | 
    
         
            +
                src = resolve_source_path(@brand_key, item['source'], @base_source_path)
         
     | 
| 
      
 31 
     | 
    
         
            +
                dst = File.join(@base_destination_path, item['destination'])
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                clean(item, src, dst)
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                return skip_empty_paths(item) if empty_paths?(item)
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                check_mandatory_file(item, src)
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                if File.exist?(src)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  copy_file(src, dst)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  git_ignore(destinations(item, src, dst))
         
     | 
| 
      
 42 
     | 
    
         
            +
                else
         
     | 
| 
      
 43 
     | 
    
         
            +
                  log_file_not_found(item)
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
              def destinations(item, src, dst, visited = {})
         
     | 
| 
      
 48 
     | 
    
         
            +
                return [] if visited[src]
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                visited[src] = true  # Mark the current source as visited
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                if File.file?(src)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  if File.directory?(dst)
         
     | 
| 
      
 54 
     | 
    
         
            +
                    return [File.join(dst, File.basename(src))]
         
     | 
| 
      
 55 
     | 
    
         
            +
                  else
         
     | 
| 
      
 56 
     | 
    
         
            +
                    return [dst]
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                elsif File.directory?(src)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  return destinations_of_directory_contents(src, dst)
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                if !item['mandatory'] && !File.file?(src)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  return get_optional_resource_destination(item, dst, visited)
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                []
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
              def get_optional_resource_destination(item, dst, visited)
         
     | 
| 
      
 70 
     | 
    
         
            +
                return [] if item['mandatory']
         
     | 
| 
      
 71 
     | 
    
         
            +
                keys = BrandsManager.instance.brands_list.map { |brand| brand['key'] }
         
     | 
| 
      
 72 
     | 
    
         
            +
                keys.each do |key|
         
     | 
| 
      
 73 
     | 
    
         
            +
                  src = resolve_source_path(key, item['source'], @base_source_path)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  result = destinations(item, src, dst, visited)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  return result unless result.empty?
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
      
 77 
     | 
    
         
            +
                []
         
     | 
| 
      
 78 
     | 
    
         
            +
              end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
              def destinations_of_directory_contents(src_dir, dst_dir)
         
     | 
| 
      
 81 
     | 
    
         
            +
                items = []
         
     | 
| 
      
 82 
     | 
    
         
            +
                Dir.foreach(src_dir) do |file|
         
     | 
| 
      
 83 
     | 
    
         
            +
                  next if file == '.' || file == '..'
         
     | 
| 
      
 84 
     | 
    
         
            +
                  full_dst_path = File.join(dst_dir, file)
         
     | 
| 
      
 85 
     | 
    
         
            +
                  items << full_dst_path
         
     | 
| 
      
 86 
     | 
    
         
            +
                end
         
     | 
| 
      
 87 
     | 
    
         
            +
                items
         
     | 
| 
      
 88 
     | 
    
         
            +
              end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
              def git_ignore(files)
         
     | 
| 
      
 91 
     | 
    
         
            +
                files.each do |file|
         
     | 
| 
      
 92 
     | 
    
         
            +
                  GitignoreManager.new(FilePath.project_root).add_items(["/#{file}"])
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
              end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
              def resolve_source_path(brand_key, source, base_source_path)
         
     | 
| 
      
 97 
     | 
    
         
            +
                source.gsub(/\{.*?\}/, brand_key).prepend(base_source_path + '/')
         
     | 
| 
      
 98 
     | 
    
         
            +
              end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
              def empty_paths?(item)
         
     | 
| 
      
 101 
     | 
    
         
            +
                item['source'].empty? || item['destination'].empty?
         
     | 
| 
      
 102 
     | 
    
         
            +
              end
         
     | 
| 
      
 103 
     | 
    
         
            +
             
     | 
| 
      
 104 
     | 
    
         
            +
              def skip_empty_paths(item)
         
     | 
| 
      
 105 
     | 
    
         
            +
                Solara.logger.debug("Skipped (empty source or destination) for #{@brand_key}: #{item['source']} -> #{item['destination']}")
         
     | 
| 
      
 106 
     | 
    
         
            +
              end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
              def check_mandatory_file(item, src)
         
     | 
| 
      
 109 
     | 
    
         
            +
                return if @ignore_health_check
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                if item['mandatory'] && !File.exist?(src)
         
     | 
| 
      
 112 
     | 
    
         
            +
                  raise "Mandatory resource file/folder not found for #{@brand_key}: #{src}. Please add the resource or mark it as not mandatory in #{FilePath.resources_manifest}."
         
     | 
| 
      
 113 
     | 
    
         
            +
                end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
              end
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
              def copy_file(src, dst)
         
     | 
| 
      
 118 
     | 
    
         
            +
                if File.directory?(src)
         
     | 
| 
      
 119 
     | 
    
         
            +
                  FileUtils.mkdir_p(dst)
         
     | 
| 
      
 120 
     | 
    
         
            +
                  FileUtils.cp_r(File.join(src, '.'), dst)
         
     | 
| 
      
 121 
     | 
    
         
            +
                else
         
     | 
| 
      
 122 
     | 
    
         
            +
                  FileUtils.mkdir_p(File.dirname(dst))
         
     | 
| 
      
 123 
     | 
    
         
            +
                  FileUtils.cp(src, dst)
         
     | 
| 
      
 124 
     | 
    
         
            +
                end
         
     | 
| 
      
 125 
     | 
    
         
            +
                Solara.logger.debug("Copied resource for #{@brand_key}: #{File.basename(src)} to #{File.basename(dst)}")
         
     | 
| 
      
 126 
     | 
    
         
            +
              end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
              def log_file_not_found(item)
         
     | 
| 
      
 129 
     | 
    
         
            +
                Solara.logger.debug("Skipped resource (not found) for #{@brand_key}: #{item['source']}")
         
     | 
| 
      
 130 
     | 
    
         
            +
              end
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
              def load_manifest_file
         
     | 
| 
      
 133 
     | 
    
         
            +
                validate_manifest_file_existence
         
     | 
| 
      
 134 
     | 
    
         
            +
                parse_manifest_file
         
     | 
| 
      
 135 
     | 
    
         
            +
              end
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
              def validate_manifest_file_existence
         
     | 
| 
      
 138 
     | 
    
         
            +
                unless File.exist?(@manifest_file)
         
     | 
| 
      
 139 
     | 
    
         
            +
                  raise "Resources manifest not found for #{@brand_key}: #{@manifest_file}"
         
     | 
| 
      
 140 
     | 
    
         
            +
                end
         
     | 
| 
      
 141 
     | 
    
         
            +
              end
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
              def parse_manifest_file
         
     | 
| 
      
 144 
     | 
    
         
            +
                begin
         
     | 
| 
      
 145 
     | 
    
         
            +
                  JSON.parse(File.read(@manifest_file))
         
     | 
| 
      
 146 
     | 
    
         
            +
                rescue JSON::ParserError => e
         
     | 
| 
      
 147 
     | 
    
         
            +
                  raise "Invalid resources manifest for #{@brand_key}: #{e.message}"
         
     | 
| 
      
 148 
     | 
    
         
            +
                end
         
     | 
| 
      
 149 
     | 
    
         
            +
              end
         
     | 
| 
      
 150 
     | 
    
         
            +
             
     | 
| 
      
 151 
     | 
    
         
            +
            end
         
     |