├── .github └── workflows │ └── dart_build.yml ├── .gitignore ├── .gitmodules ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── runConfigurations │ └── build_runner_build.xml └── vcs.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _docs └── example.png ├── analysis_options.yaml ├── asset ├── fonts │ ├── rubik-bold.ttf │ └── rubik-regular.ttf └── index.html ├── bin └── frameit_chrome.dart ├── build.yaml ├── example ├── README.md └── metadata │ ├── android │ ├── en-US │ │ ├── keyword.strings │ │ ├── samsung-galaxy-s10-plus-appbar_menu.png │ │ └── title.strings │ └── frameit.yaml │ └── framed │ └── en-US │ ├── _preview.html │ ├── samsung-galaxy-s10-plus-appbar_menu.png │ └── samsungapps-samsung-galaxy-s10-plus-appbar_menu.png ├── lib └── src │ ├── config.dart │ ├── config.g.dart │ ├── frame_colors.dart │ ├── frame_process.dart │ ├── frameit_frame.dart │ ├── scene.assetBundler.g.part │ ├── scene.dart │ └── scene.g.dart ├── pubspec.lock ├── pubspec.yaml └── tool ├── builder.dart └── src └── asset_bundler.dart /.github/workflows/dart_build.yml: -------------------------------------------------------------------------------- 1 | name: Dart Build Exe 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | # os: [macos-latest, ubuntu-latest, windows-latest] 10 | include: 11 | - os: macos-latest 12 | name: macos 13 | - os: ubuntu-latest 14 | name: linux 15 | - os: windows-latest 16 | name: windows 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | # container: 21 | # image: google/dart:latest 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: cedx/setup-dart@v2 26 | with: 27 | release-channel: dev 28 | - name: Install dependencies 29 | run: pub get 30 | - name: compile 31 | run: dart compile exe bin/frameit_chrome.dart -o frameit_chrome_${{ matrix.name }}.exe 32 | - name: Upload Artifact 33 | uses: actions/upload-artifact@v2.1.4 34 | with: 35 | # Artifact name 36 | name: frameit_chrome_${{ matrix.name }}.exe 37 | path: frameit_chrome_${{ matrix.name }}.exe 38 | # - name: Update release 39 | # if: startsWith(github.ref, 'refs/tags/v') 40 | # uses: johnwbyrd/update-release@v1 41 | # with: 42 | # token: ${{ secrets.GITHUB_TOKEN }} 43 | # draft: true 44 | # files: frameit_chrome_${{ matrix.name }}.exe 45 | #- uses: meeDamian/github-release@2.0 46 | # if: startsWith(github.ref, 'refs/tags/v') 47 | # with: 48 | # token: ${{ secrets.GITHUB_TOKEN }} 49 | # draft: true 50 | # files: frameit_chrome_${{ matrix.name }}.exe 51 | release: 52 | if: startsWith(github.ref, 'refs/tags/v') 53 | needs: build 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Download all workflow run artifacts 57 | uses: actions/download-artifact@v2 58 | - run: find . 59 | - name: Create Release 60 | id: create_release 61 | uses: actions/create-release@v1 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 64 | with: 65 | tag_name: ${{ github.ref }} 66 | draft: true 67 | prerelease: false 68 | - name: Upload Release Asset 69 | uses: actions/upload-release-asset@v1 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | with: 73 | upload_url: ${{ steps.create_release.outputs.upload_url }} 74 | asset_path: ./frameit_chrome_linux.exe/frameit_chrome_linux.exe 75 | asset_name: ./frameit_chrome_linux.exe 76 | asset_content_type: application/octet-stream 77 | - name: Upload Release Asset 78 | uses: actions/upload-release-asset@v1 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | with: 82 | upload_url: ${{ steps.create_release.outputs.upload_url }} 83 | asset_path: ./frameit_chrome_windows.exe/frameit_chrome_windows.exe 84 | asset_name: ./frameit_chrome_windows.exe 85 | asset_content_type: application/octet-stream 86 | - name: Upload Release Asset 87 | uses: actions/upload-release-asset@v1 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | with: 91 | upload_url: ${{ steps.create_release.outputs.upload_url }} 92 | asset_path: ./frameit_chrome_linux.exe/frameit_chrome_linux.exe 93 | asset_name: ./frameit_chrome_macos.exe 94 | asset_content_type: application/octet-stream 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://gist.github.com/hpoul/b78f7a1b3cde988f3ce4d12e954367eb 2 | # 3 | # IDEA: Allow some configuration, which is shared across users. 4 | 5 | /.idea/* 6 | !.idea/runConfigurations 7 | !.idea/runConfigurations/* 8 | !.idea/vcs.xml 9 | !.idea/dictionaries 10 | !.idea/dictionaries/* 11 | !.idea/inspectionProfiles/* 12 | !.idea/codeStyles 13 | !.idea/codeStyles/* 14 | *.iml 15 | 16 | # Java/Kotlin/Android 17 | 18 | /.gradle 19 | /build 20 | /out 21 | 22 | # JavaScript/Node.JS 23 | 24 | /node_modules 25 | 26 | # Dart 27 | 28 | /.dart_tool 29 | /.packages 30 | 31 | # VIM 32 | 33 | *.swp 34 | 35 | #### project specific ignores below 36 | 37 | /index_override.css 38 | 39 | /screenshot.png 40 | 41 | /example/metadata 42 | /example/metadata/* 43 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/frameit-frames"] 2 | path = deps/frameit-frames 3 | url = https://github.com/fastlane/frameit-frames.git 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 21 | 22 | 27 | 28 | 30 | 31 | 32 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | xmlns:android 41 | 42 | ^$ 43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | xmlns:.* 52 | 53 | ^$ 54 | 55 | 56 | BY_NAME 57 | 58 |
59 |
60 | 61 | 62 | 63 | .*:id 64 | 65 | http://schemas.android.com/apk/res/android 66 | 67 | 68 | 69 |
70 |
71 | 72 | 73 | 74 | .*:name 75 | 76 | http://schemas.android.com/apk/res/android 77 | 78 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | name 86 | 87 | ^$ 88 | 89 | 90 | 91 |
92 |
93 | 94 | 95 | 96 | style 97 | 98 | ^$ 99 | 100 | 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | ^$ 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 | 117 | 118 | 119 | .* 120 | 121 | http://schemas.android.com/apk/res/android 122 | 123 | 124 | ANDROID_ATTRIBUTE_ORDER 125 | 126 |
127 |
128 | 129 | 130 | 131 | .* 132 | 133 | .* 134 | 135 | 136 | BY_NAME 137 | 138 |
139 |
140 |
141 |
142 |
143 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/runConfigurations/build_runner_build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 - 2020-09-17 2 | 3 | * Initial Release 🥳 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Herbert Poul https://codeux.design/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frameit_chrome 2 | 3 | Embed app store and play store screenshots in device frames. 4 | Drop in replacement for fastlane frameit. 5 | 6 | > Also check out the blog article for [how to use frameit-chrome for flutter apps](https://codeux.design/articles/automatically-add-device-frames-and-text-to-app-screenshots/) 7 | > for more details: https://codeux.design/articles/automatically-add-device-frames-and-text-to-app-screenshots/ 8 | 9 | It uses a simple dart script to locate localized screenshots and parses 10 | `title.strings` and `keyword.strings` and uses **chrome headless** 11 | to render the screenshot with some css and html magic. 12 | 13 | [![Example Screenshot](./_docs/example.png)](./_docs/example.png) 14 | 15 | * (Screenshots from [AuthPass Password Manager](https://authpass.app/)) 16 | 17 | # Requirements 18 | 19 | * Dart 😅️ (for now) 20 | * Google Chrome executable. By default, will look into 21 | `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` 22 | (tested with Chrome 86.0.4240.30). 23 | * Screenshots and Device Frames. 24 | 25 | # Usage 26 | 27 | ## Create screenshots 28 | 29 | Use any tool to create non-framed screenshots, for flutter I've used 30 | [screenshots package](https://pub.dev/packages/screenshots). 31 | 32 | ## Download device frames 33 | 34 | Download device frames from https://github.com/fastlane/frameit-frames 35 | to `$HOME/frameit-frames`. 36 | 37 | ## Folder hierarchy 38 | 39 | Place your screenshots into file hierarchy as used by fastlane. 40 | 41 | ```bash 42 | metadata/ 43 | android/ # <-- `--base-dir` argument 44 | en-US/ 45 | -.png 46 | samsung-galaxy-s10-plus-password_generator.png # Example 47 | title.strings 48 | keyword.strings (optional) 49 | de-DE/ 50 | -.png 51 | samsung-galaxy-s10-plus-password_generator.png # Example 52 | title.strings 53 | keyword.strings (optional) 54 | frameit.yaml (optional) 55 | framed/ # <-- output directory 56 | ``` 57 | 58 | * In the above example: `` = `metadata/android` 59 | * Put Screenshots into `//images/` 60 | * a `title.strings` and `keyword.strings` into `//` 61 | * example `title.strings` (key must match part of the file name of the screenshot): 62 | ``` 63 | "password_generator" = "Great password generator!"; 64 | ``` 65 | 66 | ## Install `frameit_chrome` 67 | 68 | ```shell script 69 | pub global activate frameit_chrome 70 | ``` 71 | 72 | ## Run `frameit_chrome.dart`: 73 | 74 | (Assumes [frameit-frames](https://github.com/fastlane/frameit-frames) downloaded to `$HOME/frameit-frames`) 75 | 76 | ```shell script 77 | pub global run frameit_chrome --base-dir=/myproject/fastlane/metadata/android --frames-dir=$HOME/frameit-frames/latest 78 | ``` 79 | 80 | On non-mac platforms or when you've installed Google Chrome in non-default location: 81 | 82 | ```shell script 83 | pub global run frameit_chrome --base-dir=/myproject/fastlane/metadata/android --frames-dir=$HOME/frameit-frames/latest --chrome-binary="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" 84 | ``` 85 | 86 | # Example 87 | 88 | See the [Example Directory](./example/README.md) as well as usage of AuthPass: 89 | 90 | * Android: https://github.com/authpass/authpass/tree/master/authpass/android/fastlane/metadata/android 91 | * iOS: https://github.com/authpass/authpass/tree/master/authpass/ios/fastlane/screenshots 92 | 93 | # TODO 94 | 95 | * Allow more customizations 96 | * Frame screenshot overrides. 97 | * CSS customizations. 98 | -------------------------------------------------------------------------------- /_docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authpass/frameit-chrome/1350fcac7d53831a60ebf212c74f75647a85f9cb/_docs/example.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Defines a default set of lint rules enforced for 2 | # projects at Google. For details and rationale, 3 | # see https://github.com/dart-lang/pedantic#enabled-lints. 4 | include: package:pedantic/analysis_options.yaml 5 | 6 | analyzer: 7 | strong-mode: 8 | implicit-casts: false 9 | implicit-dynamic: false 10 | errors: 11 | # treat missing required parameters as a warning (not a hint) 12 | missing_required_param: warning 13 | # treat missing returns as a warning (not a hint) 14 | missing_return: warning 15 | # allow having TODOs in the code 16 | todo: ignore 17 | exclude: 18 | - lib/**/*.g.dart 19 | 20 | linter: 21 | rules: 22 | # these rules are documented on and in the same order as 23 | # the Dart Lint rules page to make maintenance easier 24 | # http://dart-lang.github.io/linter/lints/ 25 | 26 | # HP mostly in sync with https://github.com/flutter/flutter/blob/master/analysis_options.yaml 27 | 28 | - always_declare_return_types 29 | - always_put_control_body_on_new_line 30 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 31 | - always_require_non_null_named_parameters 32 | # - always_specify_types 33 | - annotate_overrides 34 | # - avoid_annotating_with_dynamic # not yet tested 35 | # - avoid_as 36 | - avoid_bool_literals_in_conditional_expressions 37 | # - avoid_catches_without_on_clauses # not yet tested 38 | # - avoid_catching_errors # not yet tested 39 | # - avoid_classes_with_only_static_members # not yet tested 40 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 41 | - avoid_empty_else 42 | - avoid_field_initializers_in_const_classes 43 | - avoid_function_literals_in_foreach_calls 44 | # - avoid_implementing_value_types # not yet tested 45 | - avoid_init_to_null 46 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 47 | - avoid_null_checks_in_equality_operators 48 | # - avoid_positional_boolean_parameters # not yet tested 49 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 50 | - avoid_relative_lib_imports 51 | - avoid_renaming_method_parameters 52 | - avoid_return_types_on_setters 53 | # - avoid_returning_null # not yet tested 54 | # - avoid_returning_null_for_future # not yet tested 55 | - avoid_returning_null_for_void 56 | # - avoid_returning_this # not yet tested 57 | # - avoid_setters_without_getters # not yet tested 58 | # - avoid_shadowing_type_parameters # not yet tested 59 | # - avoid_single_cascade_in_expression_statements # not yet tested 60 | - avoid_slow_async_io 61 | - avoid_types_as_parameter_names 62 | # - avoid_types_on_closure_parameters # not yet tested 63 | - avoid_unused_constructor_parameters 64 | - avoid_void_async 65 | - await_only_futures 66 | - camel_case_extensions 67 | - camel_case_types 68 | - cancel_subscriptions 69 | # - cascade_invocations # not yet tested 70 | # - close_sinks # not reliable enough 71 | # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 72 | # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 73 | - control_flow_in_finally 74 | - curly_braces_in_flow_control_structures 75 | # - diagnostic_describe_all_properties # not yet tested 76 | - directives_ordering 77 | - empty_catches 78 | - empty_constructor_bodies 79 | - empty_statements 80 | # - file_names # not yet tested 81 | # - flutter_style_todos TODO(HP) 82 | - hash_and_equals 83 | - implementation_imports 84 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 85 | - iterable_contains_unrelated_type 86 | # - join_return_with_assignment # not yet tested 87 | - library_names 88 | - library_prefixes 89 | # - lines_longer_than_80_chars # not yet tested 90 | - list_remove_unrelated_type 91 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 92 | - no_adjacent_strings_in_list 93 | - no_duplicate_case_values 94 | - non_constant_identifier_names 95 | # - null_closures # not yet tested 96 | # - omit_local_variable_types # opposite of always_specify_types 97 | # - one_member_abstracts # too many false positives 98 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 99 | - overridden_fields 100 | - package_api_docs 101 | - package_names 102 | - package_prefixed_library_names 103 | # - parameter_assignments # we do this commonly 104 | - prefer_adjacent_string_concatenation 105 | - prefer_asserts_in_initializer_lists 106 | # - prefer_asserts_with_message # not yet tested 107 | - prefer_collection_literals 108 | - prefer_conditional_assignment 109 | - prefer_const_constructors 110 | - prefer_const_constructors_in_immutables 111 | - prefer_const_declarations 112 | - prefer_const_literals_to_create_immutables 113 | # - prefer_constructors_over_static_methods # not yet tested 114 | - prefer_contains 115 | # - prefer_double_quotes # opposite of prefer_single_quotes 116 | - prefer_equal_for_default_values 117 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 118 | - prefer_final_fields 119 | - prefer_final_in_for_each 120 | - prefer_final_locals 121 | - prefer_for_elements_to_map_fromIterable 122 | - prefer_foreach 123 | # - prefer_function_declarations_over_variables # not yet tested 124 | - prefer_generic_function_type_aliases 125 | # - prefer_if_elements_to_conditional_expressions # not yet tested 126 | - prefer_if_null_operators 127 | - prefer_initializing_formals 128 | - prefer_inlined_adds 129 | # - prefer_int_literals # not yet tested 130 | # - prefer_interpolation_to_compose_strings # not yet tested 131 | - prefer_is_empty 132 | - prefer_is_not_empty 133 | - prefer_iterable_whereType 134 | # - prefer_mixin # https://github.com/dart-lang/language/issues/32 135 | # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932 136 | - prefer_single_quotes 137 | - prefer_spread_collections 138 | - prefer_typing_uninitialized_variables 139 | - prefer_void_to_null 140 | # - provide_deprecation_message # not yet tested 141 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 142 | - recursive_getters 143 | - slash_for_doc_comments 144 | # - sort_child_properties_last # not yet tested 145 | - sort_constructors_first 146 | - sort_pub_dependencies 147 | - sort_unnamed_constructors_first 148 | - test_types_in_equals 149 | - throw_in_finally 150 | # - type_annotate_public_apis # subset of always_specify_types 151 | - type_init_formals 152 | # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 153 | # - unnecessary_await_in_return # not yet tested 154 | - unnecessary_brace_in_string_interps 155 | - unnecessary_const 156 | - unnecessary_getters_setters 157 | # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 158 | - unnecessary_new 159 | - unnecessary_null_aware_assignments 160 | - unnecessary_null_in_if_null_operators 161 | - unnecessary_overrides 162 | #- unnecessary_parenthesis HP: I like parenthesis :-) 163 | - unnecessary_statements 164 | - unnecessary_this 165 | - unrelated_type_equality_checks 166 | # - unsafe_html # not yet tested 167 | - use_full_hex_values_for_flutter_colors 168 | # - use_function_type_syntax_for_parameters # not yet tested 169 | - use_rethrow_when_possible 170 | # - use_setters_to_change_properties # not yet tested 171 | # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 172 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 173 | - valid_regexps 174 | - void_checks 175 | 176 | -------------------------------------------------------------------------------- /asset/fonts/rubik-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authpass/frameit-chrome/1350fcac7d53831a60ebf212c74f75647a85f9cb/asset/fonts/rubik-bold.ttf -------------------------------------------------------------------------------- /asset/fonts/rubik-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authpass/frameit-chrome/1350fcac7d53831a60ebf212c74f75647a85f9cb/asset/fonts/rubik-regular.ttf -------------------------------------------------------------------------------- /asset/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 176 | 177 | 178 | 179 | 180 |
181 |
182 |
183 |
184 | 185 |
186 |
187 |
188 | 189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /bin/frameit_chrome.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:args/args.dart'; 5 | import 'package:frameit_chrome/src/config.dart'; 6 | import 'package:frameit_chrome/src/frame_process.dart'; 7 | import 'package:frameit_chrome/src/frameit_frame.dart'; 8 | import 'package:frameit_chrome/src/scene.dart'; 9 | import 'package:logging/logging.dart'; 10 | import 'package:logging_appenders/logging_appenders.dart'; 11 | import 'package:path/path.dart' as path; 12 | import 'package:quiver/check.dart'; 13 | import 'package:yaml/yaml.dart'; 14 | 15 | final _logger = Logger('frame'); 16 | 17 | const chromeBinaryMac = 18 | '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; 19 | 20 | const ARG_BASE_DIR = 'base-dir'; 21 | const ARG_FRAMES_DIR = 'frames-dir'; 22 | const ARG_CHROME_BINARY = 'chrome-binary'; 23 | const ARG_PIXEL_RATIO = 'pixel-ratio'; 24 | 25 | const FRAMES_REPO = 'https://github.com/fastlane/frameit-frames'; 26 | 27 | Future main(List args) async { 28 | PrintAppender.setupLogging(stderrLevel: Level.WARNING); 29 | 30 | final parser = ArgParser(); 31 | parser.addOption(ARG_BASE_DIR, 32 | help: 'base dir of screenshots. (android/fastlane/metadata/android)'); 33 | parser.addOption(ARG_FRAMES_DIR, 34 | help: 35 | 'dir with frames from $FRAMES_REPO (e.g. checkout/frameit-frames/latest)'); 36 | parser.addOption(ARG_CHROME_BINARY, 37 | help: 'Path to chrome binary.', defaultsTo: chromeBinaryMac); 38 | parser.addOption(ARG_PIXEL_RATIO, 39 | valueHelp: '2', 40 | help: 'Device pixel to real pixel ratio.', 41 | defaultsTo: '2'); 42 | final result = parser.parse(args); 43 | 44 | final baseDir = result[ARG_BASE_DIR] as String; 45 | final framesDir = result[ARG_FRAMES_DIR] as String; 46 | final chromeBinary = result[ARG_CHROME_BINARY] as String; 47 | final pixelRatio = double.tryParse(result[ARG_PIXEL_RATIO].toString()); 48 | if (baseDir == null || 49 | framesDir == null || 50 | chromeBinary == null || 51 | pixelRatio == null) { 52 | print(parser.usage); 53 | exit(1); 54 | } 55 | if (!File(chromeBinary).existsSync()) { 56 | _logger.severe('Unable to find chrome at $chromeBinary'); 57 | print(parser.usage); 58 | exit(1); 59 | } 60 | try { 61 | await runFrame(baseDir, framesDir, chromeBinary, pixelRatio); 62 | } catch (e, stackTrace) { 63 | _logger.severe('Error while creating frames.', e, stackTrace); 64 | } 65 | } 66 | 67 | final localePattern = RegExp('^[a-z]{2}-[A-Z]{2}'); 68 | 69 | Future runFrame(String baseDir, String framesDirPath, String chromeBinary, 70 | double pixelRatio) async { 71 | // validate folder. 72 | // find strings files (title.strings and keywords.strings) 73 | final dir = Directory(baseDir); 74 | checkArgument(dir.existsSync(), message: 'directory $dir does not exist.'); 75 | final outDir = Directory(path.join(dir.parent.path, 'framed')); 76 | if (outDir.existsSync()) { 77 | _logger.info('Deleting output directory $outDir'); 78 | await outDir.delete(recursive: true); 79 | } 80 | final config = await FrameConfig.load(baseDir); 81 | await outDir.create(recursive: true); 82 | final framesDir = Directory(framesDirPath); 83 | checkArgument(framesDir.existsSync(), 84 | message: '$framesDir does not exist (download $FRAMES_REPO).'); 85 | final framesProvider = await FramesProvider.create(framesDir); 86 | 87 | final tempDir = await Directory.systemTemp.createTemp('frameit_chrome'); 88 | _logger.fine('Using ${tempDir.path}'); 89 | await Assets.extractTo(tempDir); 90 | 91 | final frameProcess = FrameProcess( 92 | config: config, 93 | chromeBinary: chromeBinary, 94 | framesProvider: framesProvider, 95 | pixelRatio: pixelRatio, 96 | workingDir: Directory(path.join(tempDir.path, 'asset')), 97 | ); 98 | 99 | await for (final localeDir in dir.list()) { 100 | if (localeDir is! Directory) { 101 | // _logger.info('not a director ${localeDir}'); 102 | continue; 103 | } 104 | if (!localePattern.hasMatch(path.basename(localeDir.path))) { 105 | _logger.finer('dir is not a locale: ${path.basename(localeDir.path)}'); 106 | continue; 107 | } 108 | 109 | final titleStrings = 110 | await _parseStrings(File(path.join(localeDir.path, 'title.strings'))); 111 | final keywordStrings = await _parseStrings( 112 | File(path.join(localeDir.path, 'keyword.strings'))) ?? 113 | {}; 114 | 115 | if (titleStrings == null) { 116 | _logger.warning('Locale without titles: $localeDir'); 117 | continue; 118 | } 119 | _logger.finer('for ${path.basename(localeDir.path)} Found titles: ' 120 | '${const JsonEncoder.withIndent(' ').convert(titleStrings)}'); 121 | 122 | final imagesDir = path.join(localeDir.path); 123 | final imagesOutDir = 124 | path.join(outDir.path, path.relative(imagesDir, from: dir.path)); 125 | await frameProcess.processScreenshots( 126 | Directory(imagesDir), 127 | Directory(imagesOutDir), 128 | titleStrings, 129 | keywordStrings, 130 | ); 131 | } 132 | 133 | _logger.fine('Deleting temp directory.'); 134 | await tempDir.delete(recursive: true); 135 | } 136 | 137 | Future> _parseStrings(File file) async { 138 | final strings = {}; 139 | if (!file.existsSync()) { 140 | return null; 141 | } 142 | _logger.finest('reading ${file.path}'); 143 | final tmp = await file.readAsString(); 144 | final tmp2 = 145 | tmp.replaceAll(RegExp(r';$', multiLine: true), '').replaceAll('=', ':'); 146 | final result = loadYaml(tmp2) as Map; 147 | _logger.fine('got result: $result'); 148 | if (result == null) { 149 | return null; 150 | } 151 | for (final entry in result.entries) { 152 | strings[entry.key as String] = entry.value as String; 153 | } 154 | return strings; 155 | } 156 | 157 | // Galaxy S10: 1523x3214 158 | // iPhone XS Max: 1413x2844 159 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | sources: 4 | include: 5 | - tool/** 6 | - lib/** 7 | - asset/** 8 | - $package$ 9 | builders: 10 | builder_static_text|static_text: 11 | generate_for: 12 | - lib/**/*.dart 13 | options: 14 | content: '// ignore_for_file: implicit_dynamic_parameter,strong_mode_implicit_dynamic_parameter,strong_mode_implicit_dynamic_variable,non_constant_identifier_names,unused_element' 15 | frameit_chrome|assetBundler: 16 | generate_for: 17 | - lib/src/scene.dart 18 | options: 19 | filePattern: 'asset/**' 20 | builders: 21 | assetBundler: 22 | import: 'tool/builder.dart' 23 | builder_factories: ["assetBundler"] 24 | build_extensions: {".dart": [".g.dart"]} 25 | build_to: source 26 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example configuration for frameit_chrome 2 | 3 | This directory contains example configuration with one screenshot and 4 | two output images. 5 | 6 | `metadata/android` is the input directory with one screenshot, text and 7 | frame configuration. 8 | 9 | `metadata/frame` is the output directory. 10 | 11 | # Regenerate output 12 | 13 | To run it against `metadata/android` download [frameit-frames](https://github.com/fastlane/frameit-frames) to `$HOME/frameit-frames` and run: 14 | 15 | ```shell script 16 | pub global activate frameit_chrome 17 | pub global run frameit_chrome --base-dir=metadata/android --frames-dir=$HOME/frameit-frames/latest 18 | ``` 19 | 20 | Make sure to check out the [readme for documentation](https://github.com/authpass/frameit-chrome). 21 | -------------------------------------------------------------------------------- /example/metadata/android/en-US/keyword.strings: -------------------------------------------------------------------------------- 1 | "appbar_menu" = "SWEAT"; 2 | 3 | -------------------------------------------------------------------------------- /example/metadata/android/en-US/samsung-galaxy-s10-plus-appbar_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authpass/frameit-chrome/1350fcac7d53831a60ebf212c74f75647a85f9cb/example/metadata/android/en-US/samsung-galaxy-s10-plus-appbar_menu.png -------------------------------------------------------------------------------- /example/metadata/android/en-US/title.strings: -------------------------------------------------------------------------------- 1 | "appbar_menu" = "Example title 👍️"; 2 | 3 | -------------------------------------------------------------------------------- /example/metadata/android/frameit.yaml: -------------------------------------------------------------------------------- 1 | # Optional config to further customize frameit_chrome 2 | 3 | # Rewriting file name patterns. 4 | rewrite: 5 | - pattern: 'samsung-galaxy-s10-plus' 6 | replace: 'samsungapps-samsung-galaxy-s10-plus' 7 | action: duplicate 8 | 9 | # Customizing images. 10 | images: 11 | samsungapps-samsung-galaxy-s10-plus: 12 | cropHeight: 2600 13 | device: 'samsung-galaxy-s10-plus' 14 | previewLabel: 'Samsung App Store' 15 | css: | 16 | .scene { 17 | background: rgb(34,193,195); 18 | background: linear-gradient(0deg, rgba(34,193,195,1) 0%, rgba(253,187,45,1) 100%); 19 | } 20 | .frame-bg-shadow { 21 | filter: drop-shadow(25px 25px 15px blue); 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/metadata/framed/en-US/_preview.html: -------------------------------------------------------------------------------- 1 | present me 2 | 15 | 25 | 26 |
27 |

Samsung App Store

samsungapps-samsung-galaxy-s10-plus-appbar_menu.png

Framed Screenshots

samsung-galaxy-s10-plus-appbar_menu.png 28 | 29 | -------------------------------------------------------------------------------- /example/metadata/framed/en-US/samsung-galaxy-s10-plus-appbar_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authpass/frameit-chrome/1350fcac7d53831a60ebf212c74f75647a85f9cb/example/metadata/framed/en-US/samsung-galaxy-s10-plus-appbar_menu.png -------------------------------------------------------------------------------- /example/metadata/framed/en-US/samsungapps-samsung-galaxy-s10-plus-appbar_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authpass/frameit-chrome/1350fcac7d53831a60ebf212c74f75647a85f9cb/example/metadata/framed/en-US/samsungapps-samsung-galaxy-s10-plus-appbar_menu.png -------------------------------------------------------------------------------- /lib/src/config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:yaml/yaml.dart'; 6 | 7 | part 'config.g.dart'; 8 | 9 | @JsonSerializable(anyMap: true) 10 | class FrameConfig { 11 | FrameConfig({ 12 | @JsonKey(nullable: true) this.rewrite, 13 | @JsonKey(nullable: true) this.images, 14 | }); 15 | factory FrameConfig.fromJson(Map json) => _$FrameConfigFromJson(json); 16 | 17 | static const FILE_NAME = 'frameit.yaml'; 18 | 19 | Map toJson() => _$FrameConfigToJson(this); 20 | 21 | final List rewrite; 22 | final Map images; 23 | 24 | static Future load(String baseDir) async { 25 | final configFile = File(path.join(baseDir, FrameConfig.FILE_NAME)); 26 | if (!configFile.existsSync()) { 27 | return null; 28 | } 29 | return FrameConfig.fromJson( 30 | loadYaml(await configFile.readAsString()) as Map); 31 | } 32 | 33 | FrameImage findImageConfig(String screenshotName) { 34 | return images.entries 35 | .firstWhere((element) => screenshotName.contains(element.key), 36 | orElse: () => null) 37 | ?.value; 38 | } 39 | } 40 | 41 | enum FileAction { 42 | duplicate, 43 | exclude, 44 | rename, 45 | include, 46 | } 47 | 48 | @JsonSerializable(nullable: false, anyMap: true) 49 | class FileNameMapping { 50 | FileNameMapping({ 51 | this.pattern, 52 | this.replace, 53 | // @JsonKey(defaultValue: false) this.duplicate, 54 | // @JsonKey(defaultValue: false) this.exclude, 55 | @JsonKey(defaultValue: FileAction.rename) this.action, 56 | }); 57 | factory FileNameMapping.fromJson(Map json) => _$FileNameMappingFromJson(json); 58 | Map toJson() => _$FileNameMappingToJson(this); 59 | 60 | final String pattern; 61 | final String replace; 62 | // final bool duplicate; 63 | // final bool exclude; 64 | final FileAction action; 65 | 66 | RegExp _patternRegExp; 67 | RegExp get patternRegExp => _patternRegExp ??= RegExp(pattern); 68 | } 69 | 70 | @JsonSerializable(nullable: true, anyMap: true) 71 | class FrameImage { 72 | FrameImage({ 73 | this.cropWidth, 74 | this.cropHeight, 75 | this.device, 76 | this.previewLabel, 77 | this.css, 78 | }); 79 | factory FrameImage.fromJson(Map json) => 80 | _$FrameImageFromJson(json); 81 | Map toJson() => _$FrameImageToJson(this); 82 | 83 | /// Crop with of the final image. (null for using the original width) 84 | final int cropWidth; 85 | 86 | /// Crop height of the final image. (null for using the original width) 87 | final int cropHeight; 88 | 89 | /// device name used to look up correct frame. 90 | final String device; 91 | 92 | /// Optional label used only for the `_preview.html` 93 | final String previewLabel; 94 | 95 | /// Allows customizing the css. 96 | final String css; 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/config.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'config.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | FrameConfig _$FrameConfigFromJson(Map json) { 10 | return FrameConfig( 11 | rewrite: (json['rewrite'] as List) 12 | ?.map((e) => e == null ? null : FileNameMapping.fromJson(e as Map)) 13 | ?.toList(), 14 | images: (json['images'] as Map)?.map( 15 | (k, e) => MapEntry( 16 | k as String, 17 | e == null 18 | ? null 19 | : FrameImage.fromJson((e as Map)?.map( 20 | (k, e) => MapEntry(k as String, e), 21 | ))), 22 | ), 23 | ); 24 | } 25 | 26 | Map _$FrameConfigToJson(FrameConfig instance) => 27 | { 28 | 'rewrite': instance.rewrite, 29 | 'images': instance.images, 30 | }; 31 | 32 | FileNameMapping _$FileNameMappingFromJson(Map json) { 33 | return FileNameMapping( 34 | pattern: json['pattern'] as String, 35 | replace: json['replace'] as String, 36 | action: _$enumDecode(_$FileActionEnumMap, json['action']), 37 | ); 38 | } 39 | 40 | Map _$FileNameMappingToJson(FileNameMapping instance) => 41 | { 42 | 'pattern': instance.pattern, 43 | 'replace': instance.replace, 44 | 'action': _$FileActionEnumMap[instance.action], 45 | }; 46 | 47 | T _$enumDecode( 48 | Map enumValues, 49 | dynamic source, { 50 | T unknownValue, 51 | }) { 52 | if (source == null) { 53 | throw ArgumentError('A value must be provided. Supported values: ' 54 | '${enumValues.values.join(', ')}'); 55 | } 56 | 57 | final value = enumValues.entries 58 | .singleWhere((e) => e.value == source, orElse: () => null) 59 | ?.key; 60 | 61 | if (value == null && unknownValue == null) { 62 | throw ArgumentError('`$source` is not one of the supported values: ' 63 | '${enumValues.values.join(', ')}'); 64 | } 65 | return value ?? unknownValue; 66 | } 67 | 68 | const _$FileActionEnumMap = { 69 | FileAction.duplicate: 'duplicate', 70 | FileAction.exclude: 'exclude', 71 | FileAction.rename: 'rename', 72 | FileAction.include: 'include', 73 | }; 74 | 75 | FrameImage _$FrameImageFromJson(Map json) { 76 | return FrameImage( 77 | cropWidth: json['cropWidth'] as int, 78 | cropHeight: json['cropHeight'] as int, 79 | device: json['device'] as String, 80 | previewLabel: json['previewLabel'] as String, 81 | css: json['css'] as String, 82 | ); 83 | } 84 | 85 | Map _$FrameImageToJson(FrameImage instance) => 86 | { 87 | 'cropWidth': instance.cropWidth, 88 | 'cropHeight': instance.cropHeight, 89 | 'device': instance.device, 90 | 'previewLabel': instance.previewLabel, 91 | 'css': instance.css, 92 | }; 93 | 94 | // ************************************************************************** 95 | // StaticTextGenerator 96 | // ************************************************************************** 97 | 98 | // ignore_for_file: implicit_dynamic_parameter,strong_mode_implicit_dynamic_parameter,strong_mode_implicit_dynamic_variable,non_constant_identifier_names,unused_element 99 | -------------------------------------------------------------------------------- /lib/src/frame_colors.dart: -------------------------------------------------------------------------------- 1 | const FRAME_COLORS = { 2 | 'Matte Black', 3 | 'Space Gray', 4 | 'Rose Gold', 5 | 'Clearly White', 6 | 'Just Black', 7 | 'Not Pink', 8 | 'Silver Titanium', 9 | 'Arctic Silver', 10 | 'Coral Blue', 11 | 'Maple Gold', 12 | 'Midnight Black', 13 | 'Midnight Green', 14 | 'Orchid Gray', 15 | 'Burgundy Red', 16 | 'Lilac Purple', 17 | 'Sunrise Gold', 18 | 'Titanium Gray', 19 | 'Flamingo Pink', 20 | 'Prism Black', 21 | 'Prism Blue', 22 | 'Prism Green', 23 | 'Prism White', 24 | 'Ceramic White', 25 | 'Oh So Orange', 26 | 'Aura Black', 27 | 'Aura Glow', 28 | 'Aura Pink', 29 | 'Aura Red', 30 | 'Aura White', 31 | 'Aura Blue', 32 | 'Coral', 33 | 'Black', 34 | 'White', 35 | 'Gold', 36 | 'Silver', 37 | 'Blue', 38 | 'Red', 39 | 'Yellow', 40 | 'Green', 41 | 'Pink', 42 | 'Purple', 43 | }; 44 | -------------------------------------------------------------------------------- /lib/src/frame_process.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:frameit_chrome/src/config.dart'; 4 | import 'package:frameit_chrome/src/frameit_frame.dart'; 5 | import 'package:image/image.dart'; 6 | import 'package:logging/logging.dart'; 7 | import 'package:meta/meta.dart'; 8 | import 'package:path/path.dart' as path; 9 | import 'package:quiver/check.dart'; 10 | import 'package:supercharged_dart/supercharged_dart.dart'; 11 | 12 | final _logger = Logger('process_screenshots'); 13 | 14 | class FrameProcess { 15 | FrameProcess({ 16 | @required this.workingDir, 17 | this.config, 18 | @required this.chromeBinary, 19 | @required this.framesProvider, 20 | this.pixelRatio, 21 | }); 22 | 23 | final Directory workingDir; 24 | final FrameConfig config; 25 | final String chromeBinary; 26 | final FramesProvider framesProvider; 27 | final double pixelRatio; 28 | bool validatedPixelRatio = false; 29 | 30 | List rewriteScreenshotName(String name) { 31 | if (name.contains('framed')) { 32 | return null; 33 | } 34 | final rewrite = config?.rewrite; 35 | if (rewrite == null) { 36 | return [name]; 37 | } 38 | 39 | final ret = []; 40 | for (final r in rewrite) { 41 | final hasMatch = r.patternRegExp.hasMatch(name); 42 | if (!hasMatch) { 43 | if (r.action == FileAction.include) { 44 | return ret; 45 | } 46 | continue; 47 | } 48 | var newName = name; 49 | if (r.replace != null) { 50 | newName = name.replaceAll(r.patternRegExp, r.replace); 51 | } 52 | switch (r.action) { 53 | case FileAction.duplicate: 54 | ret.add(newName); 55 | break; 56 | case FileAction.rename: 57 | return ret..add(newName); 58 | case FileAction.exclude: 59 | return null; 60 | case FileAction.include: 61 | break; 62 | } 63 | } 64 | ret.add(name); 65 | // return name.replaceAll('samsung-galaxy-s10-plus', 'samsung-galaxy-s10'); 66 | return ret; 67 | } 68 | 69 | Future processScreenshots( 70 | Directory dir, 71 | Directory outDir, 72 | Map titleStrings, 73 | Map keywordStrings, 74 | ) async { 75 | checkArgument(dir.existsSync(), message: 'Dir does not exist $dir'); 76 | _logger.info('Processing images in $dir'); 77 | final createdScreenshots = []; 78 | await for (final fileEntity in dir.list(recursive: true)) { 79 | if (fileEntity is! File) { 80 | continue; 81 | } 82 | final file = fileEntity as File; 83 | 84 | final name = 85 | rewriteScreenshotName(path.basenameWithoutExtension(file.path)); 86 | if (name == null) { 87 | continue; 88 | } 89 | 90 | for (final variant in name) { 91 | final result = await _processScreenshot( 92 | dir, 93 | outDir, 94 | file, 95 | titleStrings, 96 | keywordStrings, 97 | variant, 98 | ); 99 | 100 | if (result != null) { 101 | createdScreenshots.add(result); 102 | } 103 | } 104 | } 105 | 106 | final imageHtml = createdScreenshots 107 | .groupBy( 108 | (element) => element.config?.previewLabel) 109 | .entries 110 | .expand((e) { 111 | e.value.sort((a, b) => a.compareTo(b)); 112 | return [ 113 | '

${e.key ?? 'Framed Screenshots'}

', 114 | ...e.value.map((e) { 115 | final src = path.relative(e.path, from: outDir.path); 116 | return '''${path.basename(e.path)}'''; 117 | }) 118 | ]; 119 | }).join(''); 120 | // createdScreenshots.sort((a, b) => a.compareTo(b)); 121 | 122 | // final imageHtml = createdScreenshots.map((e) { 123 | // final src = path.relative(e.path, from: outDir.path); 124 | // return ''''''; 125 | // }).join(''); 126 | 127 | await File(path.join(outDir.path, '_preview.html')).writeAsString(''' 128 | present me 129 | 142 | 152 | 153 |
154 | $imageHtml 155 | 156 | '''); 157 | 158 | return createdScreenshots; 159 | } 160 | 161 | Future _processScreenshot( 162 | Directory srcDir, 163 | Directory outDir, 164 | File file, 165 | Map titleStrings, 166 | Map keywordStrings, 167 | String screenshotName) async { 168 | // final outFile = path.join(file.parent.path, 169 | // '{path.basenameWithoutExtension(file.path)}_framed.png'); 170 | 171 | // find title and keyword 172 | final imageConfig = config?.findImageConfig(screenshotName); 173 | final title = _findString(titleStrings, screenshotName); 174 | final keyword = _findString(keywordStrings, screenshotName); 175 | if (title == null) { 176 | return null; 177 | } 178 | 179 | final replacedTargetName = 180 | path.join(file.parent.path, '$screenshotName.png'); 181 | final outFilePath = path.join( 182 | outDir.path, path.relative(replacedTargetName, from: srcDir.path)); 183 | await File(outFilePath).parent.create(recursive: true); 184 | 185 | final frame = framesProvider 186 | .frameForScreenshot(imageConfig?.device ?? screenshotName); 187 | _logger.fine( 188 | 'Rendering $screenshotName with title: $title ($keyword) and $frame'); 189 | 190 | final image = decodeImage(await file.readAsBytes()); 191 | 192 | final css = await _createCss( 193 | frame, 194 | image.width, 195 | image.height, 196 | screenshot: file, 197 | title: title, 198 | keyword: keyword, 199 | ) + 200 | '\n${imageConfig?.css ?? ''}\n'; 201 | final indexHtml = File(path.join(workingDir.path, 'index.html')); 202 | final cssFile = File(path.join(workingDir.path, 'index_override.css')); 203 | final screenshotFile = File(path.join(workingDir.path, 'screenshot.png')); 204 | if (screenshotFile.existsSync()) { 205 | await screenshotFile.delete(); 206 | } 207 | if (!indexHtml.existsSync()) { 208 | throw StateError('Expected index.html to be in the current directory.'); 209 | } 210 | await cssFile.writeAsString(css); 211 | final runStopwatch = Stopwatch()..start(); 212 | 213 | final width = imageConfig?.cropWidth ?? image.width; 214 | final height = imageConfig?.cropHeight ?? image.height; 215 | 216 | final result = await Process.run( 217 | chromeBinary, 218 | [ 219 | '--headless', 220 | '--no-sandbox', 221 | '--screenshot', 222 | '--hide-scrollbars', 223 | '--window-size=${width ~/ pixelRatio},${height ~/ pixelRatio}', 224 | 'index.html', 225 | ], 226 | workingDirectory: workingDir.path); 227 | if (result.exitCode != 0) { 228 | throw StateError( 229 | 'Chrome headless did not succeed. ${result.exitCode}: $result'); 230 | } 231 | 232 | if (!validatedPixelRatio) { 233 | final screenshot = decodeImage(await screenshotFile.readAsBytes()); 234 | if (screenshot.width != width) { 235 | throw StateError( 236 | 'Generated image width did not match original image width. ' 237 | 'Wrong device pixel ratio?' 238 | ' was: ${screenshot.width}' 239 | ' expected: $width' 240 | ' ratio: $pixelRatio'); 241 | } 242 | validatedPixelRatio = true; 243 | } 244 | // final screenshotResized = copyResize(screenshot, width: image.width); 245 | // await File(outFilePath).writeAsBytes(encodePng(screenshotResized)); 246 | 247 | await screenshotFile.copy(outFilePath); 248 | 249 | _logger.info('Created (${runStopwatch.elapsedMilliseconds}ms) ' 250 | '$outFilePath'); 251 | // if (srcDir.path.contains('de-DE') && outFilePath.contains('launchscreen')) { 252 | // print('DEBUG me.'); 253 | // exit(0); 254 | // } 255 | 256 | return ProcessScreenshotResult(imageConfig, outFilePath); 257 | } 258 | 259 | static String cssEscape(String str) { 260 | str = str.replaceAllMapped(RegExp('[^A-Za-z _-]+'), (match) { 261 | // str.replaceAllMapped(RegExp('[\n\t\'\"]'), (match) { 262 | final str = match.group(0); 263 | return str.runes.map((e) { 264 | return '\\${e.toRadixString(16).padLeft(6, '0')} '; 265 | }).join(''); 266 | }); 267 | return '"$str"'; 268 | } 269 | 270 | Future _createCss( 271 | Frame frame, 272 | int targetWidth, 273 | int targetHeight, { 274 | @required File screenshot, 275 | String title, 276 | String keyword, 277 | }) async { 278 | final ratio = pixelRatio; 279 | final image = decodeImage(await frame.image.readAsBytes()); 280 | final w = image.width / ratio; 281 | final h = image.height / ratio; 282 | title ??= ''; 283 | keyword ??= ''; 284 | final separator = title.isNotEmpty && keyword.isNotEmpty ? ' ' : ''; 285 | return ''' 286 | :root { 287 | --frame-orig-width: $w; 288 | --frame-orig-height: $h; 289 | 290 | --frame-orig-offset-x: ${frame.offsetX / ratio}; 291 | --frame-orig-offset-y: ${frame.offsetY / ratio}; 292 | 293 | --target-width: ${targetWidth / ratio}; 294 | --target-height: ${targetHeight / ratio}; 295 | } 296 | .keyword:before { 297 | content: ${cssEscape(keyword)}; 298 | } 299 | .keyword:after { 300 | content: '$separator'; 301 | } 302 | .title:after { 303 | content: ${cssEscape(title)}; 304 | } 305 | .screenshot-bg { 306 | background-image: url("${screenshot.absolute.path}"); 307 | } 308 | .frame-bg { 309 | background-image: url("${frame.image.absolute.path}"); 310 | } 311 | '''; 312 | } 313 | 314 | String _findString(Map strings, String filename) { 315 | for (final entry in strings.entries) { 316 | if (filename.contains(entry.key)) { 317 | return entry.value; 318 | } 319 | } 320 | return null; 321 | } 322 | } 323 | 324 | class ProcessScreenshotResult implements Comparable { 325 | ProcessScreenshotResult(this.config, this.path); 326 | 327 | final FrameImage config; 328 | final String path; 329 | 330 | @override 331 | int compareTo(ProcessScreenshotResult other) { 332 | if (config?.previewLabel != null) { 333 | if (other.config?.previewLabel != null) { 334 | return config.previewLabel.compareTo(other.config.previewLabel); 335 | } 336 | return 1; 337 | } 338 | return path.compareTo(other.path); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /lib/src/frameit_frame.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:frameit_chrome/src/frame_colors.dart'; 4 | import 'package:meta/meta.dart'; 5 | import 'package:path/path.dart' as path; 6 | 7 | import 'package:logging/logging.dart'; 8 | 9 | final _logger = Logger('frameit_frame'); 10 | 11 | String _prepareString(String str) => 12 | str.replaceAll(RegExp(r'[_-]'), ' ').toLowerCase(); 13 | 14 | class FramesProvider { 15 | FramesProvider._(this._frames); 16 | 17 | static final offsetPattern = RegExp(r'^([+-]+\d+)([+-]+\d+)'); 18 | 19 | final List _frames; 20 | 21 | static MapEntry _frameInfo( 22 | String deviceName, String fileBasename) { 23 | if (fileBasename.startsWith('Apple ') && !deviceName.startsWith('Apple ')) { 24 | fileBasename = fileBasename.replaceAll('Apple ', ''); 25 | } 26 | if (fileBasename.startsWith(deviceName)) { 27 | if (fileBasename.length > deviceName.length) { 28 | final color = fileBasename.substring(deviceName.length + 1); 29 | if (FRAME_COLORS.contains(color)) { 30 | _logger.finest('Found for $deviceName: $fileBasename'); 31 | return MapEntry(deviceName, color); 32 | } 33 | } else { 34 | return MapEntry(deviceName, null); 35 | } 36 | } 37 | return null; 38 | } 39 | 40 | static Future create(Directory baseDir) async { 41 | final frameImages = (await baseDir 42 | .list() 43 | .where((event) => event.path.endsWith('png')) 44 | .toList()) 45 | .whereType() 46 | .toList(); 47 | 48 | final offsetsFile = path.join(baseDir.path, 'offsets.json'); 49 | final offsetJson = json.decode(await File(offsetsFile).readAsString()) 50 | as Map; 51 | final offsets = 52 | (offsetJson['portrait'] as Map).entries.map((e) { 53 | final map = e.value as Map; 54 | 55 | final f = frameImages.firstWhere( 56 | (frame) => 57 | _frameInfo(e.key, path.basenameWithoutExtension(frame.path)) != 58 | null, orElse: () { 59 | _logger.warning('Cannot find ${e.key} image.'); 60 | return null; 61 | }); 62 | if (f == null) { 63 | return null; 64 | } 65 | if (!f.existsSync()) { 66 | _logger.warning('Unable to find frame image for ${e.key}'); 67 | return null; 68 | } 69 | final offsetString = map['offset'] as String; 70 | final offsetMatch = offsetPattern.firstMatch(offsetString); 71 | if (offsetMatch == null) { 72 | throw StateError('Invalid offset: $offsetString'); 73 | } 74 | // _logger.info('matches:$offsetMatch ${offsetMatch.groupCount}'); 75 | final offsetX = int.parse(offsetMatch.group(1)); 76 | final offsetY = int.parse(offsetMatch.group(2)); 77 | 78 | return Frame( 79 | name: e.key, 80 | orientation: Orientation.portrait, 81 | offsetX: offsetX, 82 | offsetY: offsetY, 83 | width: int.parse(map['width'].toString()), 84 | image: f); 85 | }); 86 | final frames = offsets.where((element) => element != null).toList(); 87 | frames.sort((a, b) => -a.nameMatch.compareTo(b.nameMatch)); 88 | return FramesProvider._(frames); 89 | } 90 | 91 | Frame frameForScreenshot(String screenshotName) { 92 | final match = _prepareString(screenshotName); 93 | return _frames.firstWhere((element) => match.contains(element.nameMatch), 94 | orElse: () { 95 | _logger.finest('unable to find frame for $match'); 96 | return null; 97 | }); 98 | } 99 | 100 | // void 101 | } 102 | 103 | enum Orientation { 104 | portrait, 105 | landscape, 106 | } 107 | 108 | class Frame { 109 | Frame({ 110 | @required this.name, 111 | @required this.orientation, 112 | @required this.offsetX, 113 | @required this.offsetY, 114 | @required this.width, 115 | @required this.image, 116 | }) : nameMatch = _prepareString(name); 117 | 118 | final String name; 119 | final String nameMatch; 120 | final Orientation orientation; 121 | final int offsetX; 122 | final int offsetY; 123 | final int width; 124 | final File image; 125 | 126 | @override 127 | String toString() { 128 | return 'Frame{name: $name, orientation: $orientation, offsetX: $offsetX, offsetY: $offsetY, width: $width, image: $image}'; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/scene.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:archive/archive.dart'; 5 | import 'package:path/path.dart' as path; 6 | 7 | part 'scene.g.dart'; 8 | 9 | class Assets { 10 | static Future extractTo(Directory baseDirectory) async { 11 | for (final asset in _Assets.all) { 12 | final f = File(path.join(baseDirectory.path, asset.fileName)); 13 | await f.parent.create(recursive: true); 14 | await f 15 | .writeAsBytes(BZip2Decoder().decodeBytes(base64.decode(asset.bytes))); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "7.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "0.39.17" 18 | archive: 19 | dependency: "direct main" 20 | description: 21 | name: archive 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.0.13" 25 | args: 26 | dependency: "direct main" 27 | description: 28 | name: args 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.6.0" 32 | async: 33 | dependency: transitive 34 | description: 35 | name: async 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.4.2" 39 | build: 40 | dependency: "direct dev" 41 | description: 42 | name: build 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.3.0" 46 | build_config: 47 | dependency: transitive 48 | description: 49 | name: build_config 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.4.2" 53 | build_daemon: 54 | dependency: transitive 55 | description: 56 | name: build_daemon 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.1.4" 60 | build_resolvers: 61 | dependency: transitive 62 | description: 63 | name: build_resolvers 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.3.11" 67 | build_runner: 68 | dependency: "direct dev" 69 | description: 70 | name: build_runner 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.10.2" 74 | build_runner_core: 75 | dependency: transitive 76 | description: 77 | name: build_runner_core 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "6.0.1" 81 | builder_static_text: 82 | dependency: "direct dev" 83 | description: 84 | name: builder_static_text 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "0.0.1+2" 88 | built_collection: 89 | dependency: transitive 90 | description: 91 | name: built_collection 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "4.3.2" 95 | built_value: 96 | dependency: transitive 97 | description: 98 | name: built_value 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "7.1.0" 102 | charcode: 103 | dependency: transitive 104 | description: 105 | name: charcode 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "1.1.3" 109 | checked_yaml: 110 | dependency: transitive 111 | description: 112 | name: checked_yaml 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "1.0.2" 116 | cli_util: 117 | dependency: transitive 118 | description: 119 | name: cli_util 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "0.2.0" 123 | clock: 124 | dependency: transitive 125 | description: 126 | name: clock 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "1.0.1" 130 | code_builder: 131 | dependency: transitive 132 | description: 133 | name: code_builder 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "3.4.1" 137 | collection: 138 | dependency: transitive 139 | description: 140 | name: collection 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "1.14.13" 144 | convert: 145 | dependency: transitive 146 | description: 147 | name: convert 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "2.1.1" 151 | crypto: 152 | dependency: transitive 153 | description: 154 | name: crypto 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "2.1.5" 158 | csslib: 159 | dependency: transitive 160 | description: 161 | name: csslib 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "0.16.2" 165 | dart_style: 166 | dependency: transitive 167 | description: 168 | name: dart_style 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "1.3.6" 172 | dio: 173 | dependency: transitive 174 | description: 175 | name: dio 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "3.0.10" 179 | fixnum: 180 | dependency: transitive 181 | description: 182 | name: fixnum 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "0.10.11" 186 | glob: 187 | dependency: "direct dev" 188 | description: 189 | name: glob 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "1.2.0" 193 | graphs: 194 | dependency: transitive 195 | description: 196 | name: graphs 197 | url: "https://pub.dartlang.org" 198 | source: hosted 199 | version: "0.2.0" 200 | html: 201 | dependency: transitive 202 | description: 203 | name: html 204 | url: "https://pub.dartlang.org" 205 | source: hosted 206 | version: "0.14.0+3" 207 | http_multi_server: 208 | dependency: transitive 209 | description: 210 | name: http_multi_server 211 | url: "https://pub.dartlang.org" 212 | source: hosted 213 | version: "2.2.0" 214 | http_parser: 215 | dependency: transitive 216 | description: 217 | name: http_parser 218 | url: "https://pub.dartlang.org" 219 | source: hosted 220 | version: "3.1.4" 221 | image: 222 | dependency: "direct main" 223 | description: 224 | name: image 225 | url: "https://pub.dartlang.org" 226 | source: hosted 227 | version: "2.1.14" 228 | intl: 229 | dependency: transitive 230 | description: 231 | name: intl 232 | url: "https://pub.dartlang.org" 233 | source: hosted 234 | version: "0.16.1" 235 | io: 236 | dependency: transitive 237 | description: 238 | name: io 239 | url: "https://pub.dartlang.org" 240 | source: hosted 241 | version: "0.3.4" 242 | js: 243 | dependency: transitive 244 | description: 245 | name: js 246 | url: "https://pub.dartlang.org" 247 | source: hosted 248 | version: "0.6.2" 249 | json_annotation: 250 | dependency: "direct main" 251 | description: 252 | name: json_annotation 253 | url: "https://pub.dartlang.org" 254 | source: hosted 255 | version: "3.0.1" 256 | json_serializable: 257 | dependency: "direct dev" 258 | description: 259 | name: json_serializable 260 | url: "https://pub.dartlang.org" 261 | source: hosted 262 | version: "3.4.1" 263 | logging: 264 | dependency: "direct main" 265 | description: 266 | name: logging 267 | url: "https://pub.dartlang.org" 268 | source: hosted 269 | version: "0.11.4" 270 | logging_appenders: 271 | dependency: "direct main" 272 | description: 273 | name: logging_appenders 274 | url: "https://pub.dartlang.org" 275 | source: hosted 276 | version: "0.4.2+5" 277 | matcher: 278 | dependency: transitive 279 | description: 280 | name: matcher 281 | url: "https://pub.dartlang.org" 282 | source: hosted 283 | version: "0.12.9" 284 | meta: 285 | dependency: "direct main" 286 | description: 287 | name: meta 288 | url: "https://pub.dartlang.org" 289 | source: hosted 290 | version: "1.2.3" 291 | mime: 292 | dependency: transitive 293 | description: 294 | name: mime 295 | url: "https://pub.dartlang.org" 296 | source: hosted 297 | version: "0.9.7" 298 | node_interop: 299 | dependency: transitive 300 | description: 301 | name: node_interop 302 | url: "https://pub.dartlang.org" 303 | source: hosted 304 | version: "1.1.1" 305 | node_io: 306 | dependency: transitive 307 | description: 308 | name: node_io 309 | url: "https://pub.dartlang.org" 310 | source: hosted 311 | version: "1.1.1" 312 | package_config: 313 | dependency: transitive 314 | description: 315 | name: package_config 316 | url: "https://pub.dartlang.org" 317 | source: hosted 318 | version: "1.9.3" 319 | path: 320 | dependency: "direct main" 321 | description: 322 | name: path 323 | url: "https://pub.dartlang.org" 324 | source: hosted 325 | version: "1.7.0" 326 | pedantic: 327 | dependency: "direct dev" 328 | description: 329 | name: pedantic 330 | url: "https://pub.dartlang.org" 331 | source: hosted 332 | version: "1.9.2" 333 | petitparser: 334 | dependency: transitive 335 | description: 336 | name: petitparser 337 | url: "https://pub.dartlang.org" 338 | source: hosted 339 | version: "3.1.0" 340 | pool: 341 | dependency: transitive 342 | description: 343 | name: pool 344 | url: "https://pub.dartlang.org" 345 | source: hosted 346 | version: "1.4.0" 347 | pub_semver: 348 | dependency: transitive 349 | description: 350 | name: pub_semver 351 | url: "https://pub.dartlang.org" 352 | source: hosted 353 | version: "1.4.4" 354 | pubspec_parse: 355 | dependency: transitive 356 | description: 357 | name: pubspec_parse 358 | url: "https://pub.dartlang.org" 359 | source: hosted 360 | version: "0.1.5" 361 | quiver: 362 | dependency: "direct main" 363 | description: 364 | name: quiver 365 | url: "https://pub.dartlang.org" 366 | source: hosted 367 | version: "2.1.3" 368 | recase: 369 | dependency: "direct dev" 370 | description: 371 | name: recase 372 | url: "https://pub.dartlang.org" 373 | source: hosted 374 | version: "3.0.0" 375 | rxdart: 376 | dependency: transitive 377 | description: 378 | name: rxdart 379 | url: "https://pub.dartlang.org" 380 | source: hosted 381 | version: "0.24.1" 382 | shelf: 383 | dependency: transitive 384 | description: 385 | name: shelf 386 | url: "https://pub.dartlang.org" 387 | source: hosted 388 | version: "0.7.9" 389 | shelf_web_socket: 390 | dependency: transitive 391 | description: 392 | name: shelf_web_socket 393 | url: "https://pub.dartlang.org" 394 | source: hosted 395 | version: "0.2.3" 396 | source_gen: 397 | dependency: "direct dev" 398 | description: 399 | name: source_gen 400 | url: "https://pub.dartlang.org" 401 | source: hosted 402 | version: "0.9.6" 403 | source_span: 404 | dependency: transitive 405 | description: 406 | name: source_span 407 | url: "https://pub.dartlang.org" 408 | source: hosted 409 | version: "1.7.0" 410 | stack_trace: 411 | dependency: transitive 412 | description: 413 | name: stack_trace 414 | url: "https://pub.dartlang.org" 415 | source: hosted 416 | version: "1.9.5" 417 | stream_channel: 418 | dependency: transitive 419 | description: 420 | name: stream_channel 421 | url: "https://pub.dartlang.org" 422 | source: hosted 423 | version: "2.0.0" 424 | stream_transform: 425 | dependency: transitive 426 | description: 427 | name: stream_transform 428 | url: "https://pub.dartlang.org" 429 | source: hosted 430 | version: "1.2.0" 431 | string_scanner: 432 | dependency: transitive 433 | description: 434 | name: string_scanner 435 | url: "https://pub.dartlang.org" 436 | source: hosted 437 | version: "1.0.5" 438 | supercharged_dart: 439 | dependency: "direct main" 440 | description: 441 | name: supercharged_dart 442 | url: "https://pub.dartlang.org" 443 | source: hosted 444 | version: "1.2.0" 445 | term_glyph: 446 | dependency: transitive 447 | description: 448 | name: term_glyph 449 | url: "https://pub.dartlang.org" 450 | source: hosted 451 | version: "1.1.0" 452 | timing: 453 | dependency: transitive 454 | description: 455 | name: timing 456 | url: "https://pub.dartlang.org" 457 | source: hosted 458 | version: "0.1.1+2" 459 | typed_data: 460 | dependency: transitive 461 | description: 462 | name: typed_data 463 | url: "https://pub.dartlang.org" 464 | source: hosted 465 | version: "1.2.0" 466 | watcher: 467 | dependency: transitive 468 | description: 469 | name: watcher 470 | url: "https://pub.dartlang.org" 471 | source: hosted 472 | version: "0.9.7+15" 473 | web_socket_channel: 474 | dependency: transitive 475 | description: 476 | name: web_socket_channel 477 | url: "https://pub.dartlang.org" 478 | source: hosted 479 | version: "1.1.0" 480 | xml: 481 | dependency: transitive 482 | description: 483 | name: xml 484 | url: "https://pub.dartlang.org" 485 | source: hosted 486 | version: "4.5.0" 487 | yaml: 488 | dependency: "direct main" 489 | description: 490 | name: yaml 491 | url: "https://pub.dartlang.org" 492 | source: hosted 493 | version: "2.2.1" 494 | sdks: 495 | dart: ">=2.10.0-0.0 <3.0.0" 496 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: frameit_chrome 2 | version: 1.0.0 3 | description: >- 4 | Embed app store and play store screenshots in device frames. 5 | Drop in replacement for fastlane frameit. 6 | homepage: https://github.com/authpass/frameit-chrome 7 | 8 | executables: 9 | frameit_chrome: frameit_chrome 10 | 11 | environment: 12 | sdk: ">=2.8.0 <3.0.0" 13 | 14 | dependencies: 15 | meta: '>=1.0.0 <2.0.0' 16 | args: ^1.6.0 17 | yaml: ^2.2.1 18 | logging: '>=0.11.3+2 <1.0.0' 19 | logging_appenders: ^0.4.2+5 20 | image: ^2.1.14 21 | # (Small) utility packages 22 | quiver: ">=2.0.0 <3.0.0" 23 | path: ^1.7.0 24 | json_annotation: ^3.0.1 25 | archive: ^2.0.13 26 | supercharged_dart: ^1.2.0 27 | 28 | dev_dependencies: 29 | pedantic: ^1.9.2 30 | json_serializable: ^3.4.1 31 | build_runner: ^1.10.2 32 | builder_static_text: ^0.0.1+2 33 | source_gen: ^0.9.6 34 | build: ">=1.3.0 <2.0.0" 35 | glob: ^1.2.0 36 | recase: ^3.0.0 37 | -------------------------------------------------------------------------------- /tool/builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:build/build.dart'; 2 | import 'package:source_gen/source_gen.dart'; 3 | 4 | import 'src/asset_bundler.dart'; 5 | 6 | extension on BuilderOptions { 7 | String requireConfig(String name) { 8 | final ret = config[name] as String; 9 | if (ret == null) { 10 | throw StateError( 11 | 'Please specify `filePattern` $config (tried to get $name)'); 12 | } 13 | return ret; 14 | } 15 | } 16 | 17 | Builder assetBundler(BuilderOptions options) => SharedPartBuilder([ 18 | AssetBundler( 19 | // dartFile: options.requireConfig('dartFile'), 20 | filePattern: options.requireConfig('filePattern'), 21 | ), 22 | ], 'assetBundler'); 23 | -------------------------------------------------------------------------------- /tool/src/asset_bundler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:archive/archive.dart'; 5 | import 'package:build/build.dart'; 6 | import 'package:glob/glob.dart'; 7 | import 'package:meta/meta.dart'; 8 | import 'package:recase/recase.dart'; 9 | import 'package:source_gen/source_gen.dart'; 10 | 11 | class AssetBundler extends Generator { 12 | AssetBundler({@required this.filePattern}); 13 | 14 | // final String dartFile; 15 | final String filePattern; 16 | 17 | @override 18 | FutureOr generate(LibraryReader library, BuildStep buildStep) async { 19 | // print('bundle for ${buildStep.inputId.path} ?'); 20 | // if (!buildStep.inputId.path.endsWith(dartFile)) { 21 | // return null; 22 | // } 23 | // print('looking for $filePattern'); 24 | final ret = StringBuffer(''' 25 | class _Asset { 26 | const _Asset(this.fileName, this.bytes); 27 | final String fileName; 28 | final String bytes; 29 | } 30 | 31 | 32 | class _Assets { 33 | '''); 34 | 35 | final assetList = []; 36 | await for (final asset in buildStep.findAssets(Glob(filePattern))) { 37 | // print('should bundle ${asset.path}'); 38 | final name = asset.path.camelCase; 39 | final bytes = await buildStep.readAsBytes(asset); 40 | final compressed = BZip2Encoder().encode(bytes); 41 | final base64 = base64Encode(compressed); 42 | ret.writeln( 43 | '''static const $name = _Asset('${asset.path}', '$base64');'''); 44 | assetList.add(name); 45 | } 46 | 47 | ret.writeln('static const all = [${assetList.join(',')}];'); 48 | 49 | ret.writeln('}'); 50 | return ret.toString(); 51 | } 52 | } 53 | --------------------------------------------------------------------------------