├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── bsconfig.json ├── package.json ├── react-native.config.js ├── src └── generate.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.bat] 11 | end_of_line = crlf 12 | 13 | [*.{gradle,xml}] 14 | indent_size = 4 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Windows files should use crlf line endings 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | *.hprof 32 | 33 | # Visual Studio Code 34 | # 35 | .vscode/ 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # BUCK 44 | buck-out/ 45 | \.buckd/ 46 | *.keystore 47 | !debug.keystore 48 | 49 | # fastlane 50 | # 51 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 52 | # screenshots whenever they are needed. 53 | # For more information about the recommended setup visit: 54 | # https://docs.fastlane.tools/best-practices/source-control/ 55 | 56 | */fastlane/report.xml 57 | */fastlane/Preview.html 58 | */fastlane/screenshots 59 | 60 | # Bundle artifact 61 | *.jsbundle 62 | 63 | # CocoaPods 64 | Pods/ 65 | 66 | # Bob 67 | dist/ 68 | 69 | # OCaml / Reason 70 | **/.merlin 71 | **/lib/bs 72 | **/lib/ocaml 73 | *.bs.js 74 | .bsb.lock 75 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | Pods/ 4 | example/android/app/build/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mathieu Acthernoene 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 | # react-native-bootsplash-cli-fork 2 | 3 | [![npm version](https://badge.fury.io/js/react-native-bootsplash-cli-fork.svg)](https://www.npmjs.org/package/react-native-bootsplash-cli-fork) 4 | [![npm](https://img.shields.io/npm/dt/react-native-bootsplash-cli-fork.svg)](https://www.npmjs.org/package/react-native-bootsplash-cli-fork) 5 | [![MIT](https://img.shields.io/dub/l/vibe-d.svg)](https://opensource.org/licenses/MIT) 6 | [![Platform - Android](https://img.shields.io/badge/platform-Android-3ddc84.svg?style=flat&logo=android)](https://www.android.com) 7 | [![Platform - iOS](https://img.shields.io/badge/platform-iOS-000.svg?style=flat&logo=apple)](https://developer.apple.com/ios) 8 | 9 | This is a fork of [react-native-bootsplash](https://github.com/zoontek/react-native-bootsplash) CLI. 10 | Fork allows generating dark mode splash screens. 11 | Fork is based on original react-native-bootsplash CLI with following changes: 12 | - New parameter: --dark-logo [path], allows to specify different logo for dark mode themes 13 | - New parameter: --dark-background-color [color], allows to specify different background color for dark mode themes 14 | 15 | Note: this is NOT a fork/replacement of react-native-bootsplash itself! You still need to install and configure [react-native-bootsplash according to instructions](https://github.com/zoontek/react-native-bootsplash#ios-1). 16 | This library could be used as a replacement of [Assets generation 17 | ](https://github.com/zoontek/react-native-bootsplash#assets-generation) step. 18 | 19 | ## Installation 20 | 21 | ```bash 22 | $ npm install --save-dev react-native-bootsplash-cli-fork 23 | # --- or --- 24 | $ yarn add -D react-native-bootsplash-cli-fork 25 | ``` 26 | 27 | ## Usage 28 | 29 | **CLI** could generate assets, create the Android Drawable XML file and the iOS Storyboard file automatically ✨. 30 | 31 | ```bash 32 | $ npx react-native generate-bootsplash-fork --help 33 | # --- or --- 34 | $ yarn react-native generate-bootsplash-fork --help 35 | ``` 36 | 37 | The command can take multiple arguments: 38 | 39 | ```bash 40 | yarn react-native generate-bootsplash-fork 41 | 42 | Generate a launch screen using an original logo file 43 | 44 | Options: 45 | --background-color color used as launch screen background (in hexadecimal format) (default: "#fff") 46 | --logo-width logo width at @1x (in dp - we recommend approximately ~100) (default: 100) 47 | --assets-path [path] path to your static assets directory (useful to require the logo file in JS) 48 | --flavor [android only] flavor build variant (outputs in an android resource directory other than "main") 49 | --dark-logo [path] [optional] if specified, will be used for splashscreen that is shown when phone is in dark mode 50 | --dark-background-color [optional] color used as launch screen background when phone is in dark mode (in hexadecimal format) (default: "#000"). Only used if --dark-logo-path is set! 51 | -h, --help output usage information 52 | ``` 53 | 54 | #### Full command usage example 55 | 56 | ```bash 57 | yarn react-native generate-bootsplash assets/bootsplash_logo_original.png \ 58 | --background-color=F5FCFF \ 59 | --dark-logo=assets/bootsplash_logo_dark.png \ 60 | --logo-width=100 \ 61 | --assets-path=assets \ 62 | --flavor=main 63 | ``` 64 | 65 | This tool generates assets that could later be used by react-native-bootsplash: 66 | ```bash 67 | # Only if --assets-path was specified 68 | assets/bootsplash_logo.png 69 | assets/bootsplash_logo@1,5x.png 70 | assets/bootsplash_logo@2x.png 71 | assets/bootsplash_logo@3x.png 72 | assets/bootsplash_logo@4x.png 73 | # if dark logo is specified 74 | assets/bootsplash_logo_dark.png 75 | assets/bootsplash_logo_dark@1,5x.png 76 | assets/bootsplash_logo_dark@2x.png 77 | assets/bootsplash_logo_dark@3x.png 78 | assets/bootsplash_logo_dark@4x.png 79 | 80 | android/app/src/main/res/values/colors.xml (creation and addition) 81 | android/app/src/main/res/values-night/colors.xml 82 | android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png 83 | android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png 84 | android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png 85 | android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png 86 | android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png 87 | # if dark logo is specified 88 | android/app/src/main/res/mipmap-hdpi/bootsplash_logo_dark.png 89 | android/app/src/main/res/mipmap-mdpi/bootsplash_logo_dark.png 90 | android/app/src/main/res/mipmap-xhdpi/bootsplash_logo_dark.png 91 | android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo_dark.png 92 | android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo_dark.png 93 | 94 | 95 | ios/YourProjectName/BootSplash.storyboard 96 | ios/YourProjectName/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png 97 | ios/YourProjectName/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png 98 | ios/YourProjectName/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png 99 | # if dark logo is specified 100 | ios/YourProjectName/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo_dark.png 101 | ios/YourProjectName/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo_dark@2x.png 102 | ios/YourProjectName/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo_dark@3x.png 103 | ``` 104 | 105 | If you want to have different splashscreen in Dark Mode, also create/edit the `android/app/src/main/res/values-night/styles.xml`: 106 | 107 | ```xml 108 | 109 | 110 | 115 | 116 | 117 | ``` 118 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-bootsplash-cli-fork", 3 | "package-specs": { 4 | "module": "es6", 5 | "in-source": true 6 | }, 7 | "suffix": ".bs.js", 8 | "sources": [ 9 | { 10 | "dir": ".", 11 | "subdirs": false 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-bootsplash-cli-fork", 3 | "version": "4.5.2", 4 | "license": "MIT", 5 | "description": "Fork of CLI for generating assets for react-native-bootsplash.", 6 | "author": "Mathieu Acthernoene ", 7 | "homepage": "https://github.com/shaddix/react-native-bootsplash-cli-fork", 8 | "main": "dist/commonjs/index.js", 9 | "module": "dist/module/index.js", 10 | "types": "dist/typescript/index.d.ts", 11 | "files": [ 12 | "/dist", 13 | "/src", 14 | "bsconfig.json", 15 | "package.json", 16 | "react-native.config.js" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/shaddix/react-native-bootsplash-cli-fork.git" 21 | }, 22 | "keywords": [ 23 | "react-native-bootsplash", 24 | "react-native", 25 | "boot-splash", 26 | "bootsplash", 27 | "boot-screen", 28 | "bootscreen", 29 | "splash-screen", 30 | "splashscreen", 31 | "launch-screen", 32 | "launchscreen" 33 | ], 34 | "scripts": { 35 | "format": "prettier '**/*.{js,json,md,ts,tsx}' --write", 36 | "setup-hooks": "git config --local core.hooksPath .hooks", 37 | "prepare": "yarn setup-hooks && bob build", 38 | "do-publish": "yarn publish" 39 | }, 40 | "react-native-builder-bob": { 41 | "source": "src", 42 | "output": "dist", 43 | "targets": [ 44 | "commonjs", 45 | "module", 46 | "typescript" 47 | ] 48 | }, 49 | "prettier": { 50 | "trailingComma": "all" 51 | }, 52 | "lint-staged": { 53 | "**/*.{js,json,md,ts,tsx}": "prettier --write" 54 | }, 55 | "peerDependencies": { 56 | "react-native": ">=0.65.0" 57 | }, 58 | "dependencies": { 59 | "fs-extra": "^11.1.0", 60 | "sharp": "^0.31.3", 61 | "picocolors": "^1.0.0" 62 | }, 63 | "devDependencies": { 64 | "@babel/core": "^7.21.0", 65 | "@types/fs-extra": "^11.0.1", 66 | "@types/react": "^18.0.28", 67 | "@types/sharp": "^0.31.1", 68 | "lint-staged": "^13.1.2", 69 | "prettier": "^2.8.4", 70 | "prettier-plugin-organize-imports": "^3.2.2", 71 | "react": "18.2.0", 72 | "react-native": "0.71.3", 73 | "react-native-builder-bob": "^0.20.4", 74 | "rescript": "^10.1.2", 75 | "typescript": "^4.9.5" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { generate } = require("./dist/commonjs/generate"); 3 | 4 | module.exports = { 5 | commands: [ 6 | { 7 | name: "generate-bootsplash-fork ", 8 | description: "Generate a launch screen using an original logo file", 9 | options: [ 10 | { 11 | name: "--background-color ", 12 | description: 13 | "color used as launch screen background (in hexadecimal format)", 14 | default: "#fff", 15 | }, 16 | { 17 | name: "--logo-width ", 18 | description: 19 | "logo width at @1x (in dp - we recommend approximately ~100)", 20 | default: 100, 21 | parse: (value) => parseInt(value, 10), 22 | }, 23 | { 24 | name: "--assets-path [path]", 25 | description: 26 | "path to your static assets directory (useful to require the logo file in JS)", 27 | }, 28 | { 29 | name: "--flavor ", 30 | description: 31 | '[android only] flavor build variant (outputs in an android resource directory other than "main")', 32 | default: "main", 33 | }, 34 | { 35 | name: "--dark-logo [path]", 36 | description: 37 | "path to logo that should be used in dark mode (dark logos are not generated by default)", 38 | default: "", 39 | }, 40 | { 41 | name: "--dark-background-color ", 42 | description: 43 | "color used as launch screen background for dark theme (in hexadecimal format). Only used if --dark-logo is specified", 44 | default: "#000", 45 | }, 46 | ], 47 | func: ( 48 | [logoPath], 49 | { project: { android, ios } }, 50 | { 51 | backgroundColor, 52 | logoWidth, 53 | assetsPath, 54 | flavor, 55 | darkLogo, 56 | darkBackgroundColor, 57 | }, 58 | ) => { 59 | const workingPath = 60 | process.env.INIT_CWD || process.env.PWD || process.cwd(); 61 | 62 | if (logoWidth > 288) { 63 | console.log( 64 | "❌ Logo width can't be superior to 288dp as it will be cropped on Android. Exiting…\n", 65 | ); 66 | 67 | process.exit(1); 68 | } else if (logoWidth > 192) { 69 | console.log( 70 | "⚠️ As logo width is superior to 192dp, it might be cropped on Android.\n", 71 | ); 72 | } 73 | 74 | return generate({ 75 | android, 76 | ios: ios 77 | ? { 78 | ...ios, 79 | // Fix to support previous CLI versions 80 | projectPath: (ios.xcodeProject 81 | ? path.resolve(ios.sourceDir, ios.xcodeProject.name) 82 | : ios.projectPath 83 | ).replace(/\.(xcodeproj|xcworkspace)$/, ""), 84 | } 85 | : null, 86 | workingPath, 87 | logoPath: path.resolve(workingPath, logoPath), 88 | darkLogoPath: darkLogo 89 | ? path.resolve(workingPath, darkLogo) 90 | : undefined, 91 | assetsPath: assetsPath 92 | ? path.resolve(workingPath, assetsPath) 93 | : undefined, 94 | 95 | backgroundColor, 96 | darkBackgroundColor, 97 | flavor, 98 | logoWidth, 99 | }).catch((error) => { 100 | console.error(error); 101 | }); 102 | }, 103 | }, 104 | ], 105 | }; 106 | -------------------------------------------------------------------------------- /src/generate.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import fs from "fs-extra"; 3 | import path from "path"; 4 | import pc from "picocolors"; 5 | import sharp from "sharp"; 6 | 7 | const lightLogoFileName = "bootsplash_logo"; 8 | const darkLogoFileName = "bootsplash_logo_dark"; 9 | const logoAssetName = "BootSplashLogo"; 10 | const colorAssetName = "SplashColor"; 11 | const androidColorName = "bootsplash_background"; 12 | const androidColorRegex = /#\w+<\/color>/g; 13 | 14 | const toFullHexadecimal = (hex: string) => { 15 | const prefixed = hex[0] === "#" ? hex : `#${hex}`; 16 | const up = prefixed.toUpperCase(); 17 | 18 | return up.length === 4 19 | ? "#" + up[1] + up[1] + up[2] + up[2] + up[3] + up[3] 20 | : up; 21 | }; 22 | 23 | const colorToRGB = (hex: string) => { 24 | const fullHexColor = toFullHexadecimal(hex); 25 | 26 | return { 27 | r: (parseInt(fullHexColor[1] + fullHexColor[2], 16) / 255).toPrecision(15), 28 | g: (parseInt(fullHexColor[3] + fullHexColor[4], 16) / 255).toPrecision(15), 29 | b: (parseInt(fullHexColor[5] + fullHexColor[6], 16) / 255).toPrecision(15), 30 | }; 31 | }; 32 | 33 | const getLogoContentsJson = (includeDarkLogo: boolean) => `{ 34 | "images": [ 35 | { 36 | "idiom": "universal", 37 | "filename": "${lightLogoFileName}.png", 38 | "scale": "1x" 39 | }, 40 | { 41 | "idiom": "universal", 42 | "filename": "${lightLogoFileName}@2x.png", 43 | "scale": "2x" 44 | }, 45 | { 46 | "idiom": "universal", 47 | "filename": "${lightLogoFileName}@3x.png", 48 | "scale": "3x" 49 | }${includeDarkLogo ? DarkImagesContentsJson : ""} 50 | ], 51 | "info": { 52 | "version": 1, 53 | "author": "xcode" 54 | } 55 | } 56 | `; 57 | const DarkImagesContentsJson = `, 58 | { 59 | "appearances" : [ 60 | { 61 | "appearance" : "luminosity", 62 | "value" : "dark" 63 | } 64 | ], 65 | "idiom": "universal", 66 | "filename": "${darkLogoFileName}.png", 67 | "scale": "1x" 68 | }, 69 | { 70 | "appearances" : [ 71 | { 72 | "appearance" : "luminosity", 73 | "value" : "dark" 74 | } 75 | ], 76 | "idiom": "universal", 77 | "filename": "${darkLogoFileName}@2x.png", 78 | "scale": "2x" 79 | }, 80 | { 81 | "appearances" : [ 82 | { 83 | "appearance" : "luminosity", 84 | "value" : "dark" 85 | } 86 | ], 87 | "idiom": "universal", 88 | "filename": "${darkLogoFileName}@3x.png", 89 | "scale": "3x" 90 | } 91 | `; 92 | 93 | const getDarkColorsContentsJson = (darkColor: string) => { 94 | const rgb = colorToRGB(darkColor); 95 | return `, 96 | { 97 | "appearances" : [ 98 | { 99 | "appearance" : "luminosity", 100 | "value" : "dark" 101 | } 102 | ], 103 | "color" : { 104 | "color-space" : "srgb", 105 | "components" : { 106 | "alpha" : "1.000", 107 | "blue" : "${rgb.b}", 108 | "green" : "${rgb.g}", 109 | "red" : "${rgb.r}" 110 | } 111 | }, 112 | "idiom" : "universal" 113 | } 114 | `; 115 | }; 116 | 117 | const getColorsContentsJson = (lightColor: string, darkColor?: string) => { 118 | const rgb = colorToRGB(lightColor); 119 | return `{ 120 | "colors" : [ 121 | { 122 | "color" : { 123 | "color-space" : "srgb", 124 | "components" : { 125 | "alpha" : "1.000", 126 | "blue" : "${rgb.b}", 127 | "green" : "${rgb.g}", 128 | "red" : "${rgb.r}" 129 | } 130 | }, 131 | "idiom" : "universal" 132 | }${darkColor ? getDarkColorsContentsJson(darkColor) : ""} 133 | ], 134 | "info" : { 135 | "author" : "xcode", 136 | "version" : 1 137 | } 138 | }`; 139 | }; 140 | 141 | const getStoryboard = ({ 142 | height, 143 | width, 144 | }: { 145 | height: number; 146 | width: number; 147 | }) => { 148 | return ` 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | `; 198 | }; 199 | 200 | const log = { 201 | error: (text: string) => console.log(pc.red(text)), 202 | text: (text: string) => console.log(text), 203 | warn: (text: string) => console.log(pc.yellow(text)), 204 | }; 205 | 206 | const logWriteGlobal = ( 207 | emoji: string, 208 | filePath: string, 209 | workingPath: string, 210 | dimensions?: { width: number; height: number }, 211 | ) => 212 | log.text( 213 | `${emoji} ${path.relative(workingPath, filePath)}` + 214 | (dimensions != null ? ` (${dimensions.width}x${dimensions.height})` : ""), 215 | ); 216 | 217 | const isValidHexadecimal = (value: string) => 218 | /^#?([0-9A-F]{3}){1,2}$/i.test(value); 219 | 220 | export const generate = async ({ 221 | android, 222 | ios, 223 | 224 | workingPath, 225 | logoPath, 226 | darkLogoPath, 227 | backgroundColor, 228 | darkBackgroundColor, 229 | logoWidth, 230 | flavor, 231 | assetsPath, 232 | }: { 233 | android: { 234 | sourceDir: string; 235 | appName: string; 236 | } | null; 237 | ios: { 238 | projectPath: string; 239 | } | null; 240 | 241 | workingPath: string; 242 | logoPath: string; 243 | darkLogoPath?: string; 244 | assetsPath?: string; 245 | 246 | backgroundColor: string; 247 | darkBackgroundColor?: string; 248 | flavor: string; 249 | logoWidth: number; 250 | }) => { 251 | 252 | await generateSingle({ 253 | android, 254 | ios, 255 | 256 | workingPath, 257 | logoPath, 258 | backgroundColor, 259 | logoWidth, 260 | flavor, 261 | assetsPath, 262 | theme: "light", 263 | }); 264 | 265 | if (darkLogoPath && darkBackgroundColor) { 266 | await generateSingle({ 267 | android, 268 | ios, 269 | 270 | workingPath, 271 | logoPath: darkLogoPath, 272 | backgroundColor: darkBackgroundColor, 273 | logoWidth, 274 | flavor, 275 | assetsPath, 276 | theme: "dark", 277 | }); 278 | } 279 | 280 | if (ios) { 281 | createIosAssets({ 282 | projectPath: ios.projectPath, 283 | workingPath, 284 | includeDarkLogo: !!darkLogoPath, 285 | lightBackgroundColor: backgroundColor, 286 | darkBackgroundColor: darkBackgroundColor, 287 | }); 288 | } 289 | 290 | log.text(` 291 | ${chalk.blue("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")} 292 | ${chalk.blue("┃")} 💖 ${chalk.bold( 293 | "Love this library? Consider sponsoring!", 294 | )} ${chalk.blue("┃")} 295 | ${chalk.blue("┃")} One-time amounts are available. ${chalk.blue( 296 | "┃", 297 | )} 298 | ${chalk.blue("┃")} ${chalk.underline( 299 | "https://github.com/sponsors/zoontek", 300 | )} ${chalk.blue("┃")} 301 | ${chalk.blue("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")} 302 | `); 303 | 304 | log.text( 305 | `✅ Done! Thanks for using ${chalk.underline("react-native-bootsplash")}.`, 306 | ); 307 | }; 308 | 309 | const generateSingle = async ({ 310 | android, 311 | ios, 312 | 313 | workingPath, 314 | logoPath, 315 | backgroundColor, 316 | logoWidth, 317 | flavor, 318 | assetsPath, 319 | theme, 320 | }: { 321 | android: { 322 | sourceDir: string; 323 | appName: string; 324 | } | null; 325 | ios: { 326 | projectPath: string; 327 | } | null; 328 | 329 | workingPath: string; 330 | logoPath: string; 331 | assetsPath?: string; 332 | 333 | backgroundColor: string; 334 | flavor: string; 335 | logoWidth: number; 336 | theme: "light" | "dark"; 337 | }) => { 338 | if (!isValidHexadecimal(backgroundColor)) { 339 | log.error("--background-color value is not a valid hexadecimal color."); 340 | process.exit(1); 341 | } 342 | 343 | const logoFileName = theme === "light" ? lightLogoFileName : darkLogoFileName; 344 | 345 | const image = sharp(logoPath); 346 | const backgroundColorHex = toFullHexadecimal(backgroundColor); 347 | const { format } = await image.metadata(); 348 | 349 | if (format !== "png" && format !== "svg") { 350 | log.error("Input file is an unsupported image format"); 351 | process.exit(1); 352 | } 353 | 354 | const logoHeight = await image 355 | .clone() 356 | .resize(logoWidth) 357 | .toBuffer() 358 | .then((buffer) => sharp(buffer).metadata()) 359 | .then(({ height = 0 }) => height); 360 | 361 | const shouldSkipAndroid = logoWidth > 288 || logoHeight > 288; 362 | 363 | const logAbove288 = (dimension: "height" | "width") => { 364 | const message = `⚠️ Logo ${dimension} exceed 288dp. As it will be cropped by Android, we skip generation for this platform.`; 365 | log.warn(message); 366 | }; 367 | 368 | const logAbove192 = (dimension: "height" | "width") => { 369 | const message = `⚠️ Logo ${dimension} exceed 192dp. It might be cropped by Android.`; 370 | log.warn(message); 371 | }; 372 | 373 | if (logoWidth > 288) { 374 | logAbove288("width"); 375 | } else if (logoHeight > 288) { 376 | logAbove288("height"); 377 | } else if (logoWidth > 192) { 378 | logAbove192("width"); 379 | } else if (logoHeight > 192) { 380 | logAbove192("height"); 381 | } 382 | 383 | const logWrite = ( 384 | emoji: string, 385 | filePath: string, 386 | dimensions?: { width: number; height: number }, 387 | ) => logWriteGlobal(emoji, filePath, workingPath, dimensions); 388 | 389 | if (assetsPath && fs.existsSync(assetsPath)) { 390 | log.text(`\n ${chalk.underline("Assets")}`); 391 | 392 | await Promise.all( 393 | [ 394 | { ratio: 1, suffix: "" }, 395 | { ratio: 1.5, suffix: "@1,5x" }, 396 | { ratio: 2, suffix: "@2x" }, 397 | { ratio: 3, suffix: "@3x" }, 398 | { ratio: 4, suffix: "@4x" }, 399 | ].map(({ ratio, suffix }) => { 400 | const fileName = `${logoFileName}${suffix}.png`; 401 | const filePath = path.resolve(assetsPath, fileName); 402 | 403 | return image 404 | .clone() 405 | .resize(logoWidth * ratio) 406 | .png({ quality: 100 }) 407 | .toFile(filePath) 408 | .then(({ width, height }) => { 409 | logWrite("✨", filePath, { width, height }); 410 | }); 411 | }), 412 | ); 413 | } 414 | 415 | if (android && !shouldSkipAndroid) { 416 | log.text(`\n ${pc.underline("Android")}`); 417 | 418 | 419 | const appPath = android.appName 420 | ? path.resolve(android.sourceDir, android.appName) 421 | : path.resolve(android.sourceDir); // @react-native-community/cli 2.x & 3.x support 422 | 423 | const resPath = path.resolve(appPath, "src", flavor, "res"); 424 | const valuesPath = path.resolve( 425 | resPath, 426 | theme === "light" ? "values" : "values-night", 427 | ); 428 | 429 | fs.ensureDirSync(valuesPath); 430 | 431 | const colorsXmlPath = path.resolve(valuesPath, "colors.xml"); 432 | const colorsXmlEntry = `${backgroundColorHex}`; 433 | 434 | if (fs.existsSync(colorsXmlPath)) { 435 | const colorsXml = fs.readFileSync(colorsXmlPath, "utf-8"); 436 | 437 | if (colorsXml.match(androidColorRegex)) { 438 | fs.writeFileSync( 439 | colorsXmlPath, 440 | colorsXml.replace(androidColorRegex, colorsXmlEntry), 441 | "utf-8", 442 | ); 443 | } else { 444 | fs.writeFileSync( 445 | colorsXmlPath, 446 | colorsXml.replace( 447 | /<\/resources>/g, 448 | ` ${colorsXmlEntry}\n`, 449 | ), 450 | "utf-8", 451 | ); 452 | } 453 | 454 | logWrite("✏️ ", colorsXmlPath); 455 | } else { 456 | fs.writeFileSync( 457 | colorsXmlPath, 458 | `\n ${colorsXmlEntry}\n\n`, 459 | "utf-8", 460 | ); 461 | 462 | logWrite("✨", colorsXmlPath); 463 | } 464 | 465 | await Promise.all( 466 | [ 467 | { ratio: 1, directory: "mipmap-mdpi" }, 468 | { ratio: 1.5, directory: "mipmap-hdpi" }, 469 | { ratio: 2, directory: "mipmap-xhdpi" }, 470 | { ratio: 3, directory: "mipmap-xxhdpi" }, 471 | { ratio: 4, directory: "mipmap-xxxhdpi" }, 472 | ].map(({ ratio, directory }) => { 473 | const fileName = `${logoFileName}.png`; 474 | const filePath = path.resolve(resPath, directory, fileName); 475 | // https://github.com/androidx/androidx/blob/androidx-main/core/core-splashscreen/src/main/res/values/dimens.xml#L22 476 | const canvasSize = 288 * ratio; 477 | 478 | // https://sharp.pixelplumbing.com/api-constructor 479 | const canvas = sharp({ 480 | create: { 481 | width: canvasSize, 482 | height: canvasSize, 483 | channels: 4, 484 | background: { 485 | r: 255, 486 | g: 255, 487 | b: 255, 488 | alpha: 0, 489 | }, 490 | }, 491 | }); 492 | 493 | return image 494 | .clone() 495 | .resize(logoWidth * ratio) 496 | .toBuffer() 497 | .then((input) => 498 | canvas 499 | .composite([{ input }]) 500 | .png({ quality: 100 }) 501 | .toFile(filePath), 502 | ) 503 | .then(() => { 504 | logWrite("✨", filePath, { width: canvasSize, height: canvasSize }); 505 | }); 506 | }), 507 | ); 508 | } 509 | 510 | if (ios) { 511 | log.text(`\n ${chalk.underline("iOS")}`); 512 | const projectPath = ios.projectPath; 513 | const imagesPath = path.resolve(projectPath, "Images.xcassets"); 514 | 515 | if (fs.existsSync(projectPath)) { 516 | const storyboardPath = path.resolve(projectPath, "BootSplash.storyboard"); 517 | 518 | fs.writeFileSync( 519 | storyboardPath, 520 | getStoryboard({ 521 | height: logoHeight, 522 | width: logoWidth, 523 | }), 524 | "utf-8", 525 | ); 526 | 527 | logWrite("✨", storyboardPath); 528 | } else { 529 | log.text( 530 | `No "${projectPath}" directory found. Skipping iOS storyboard generation…`, 531 | ); 532 | } 533 | 534 | if (fs.existsSync(imagesPath)) { 535 | const imageSetPath = path.resolve( 536 | imagesPath, 537 | logoAssetName + ".imageset", 538 | ); 539 | fs.ensureDirSync(imageSetPath); 540 | 541 | await Promise.all( 542 | [ 543 | { ratio: 1, suffix: "" }, 544 | { ratio: 2, suffix: "@2x" }, 545 | { ratio: 3, suffix: "@3x" }, 546 | ].map(({ ratio, suffix }) => { 547 | const fileName = `${logoFileName}${suffix}.png`; 548 | const filePath = path.resolve(imageSetPath, fileName); 549 | 550 | return image 551 | .clone() 552 | .resize(logoWidth * ratio) 553 | .png({ quality: 100 }) 554 | .toFile(filePath) 555 | .then(({ width, height }) => { 556 | logWrite("✨", filePath, { width, height }); 557 | }); 558 | }), 559 | ); 560 | } else { 561 | log.text( 562 | `No "${imagesPath}" directory found. Skipping iOS images generation…`, 563 | ); 564 | } 565 | } 566 | }; 567 | 568 | const createIosAssets = ({ 569 | projectPath, 570 | workingPath, 571 | 572 | includeDarkLogo, 573 | lightBackgroundColor, 574 | darkBackgroundColor, 575 | }: { 576 | projectPath: string; 577 | workingPath: string; 578 | 579 | includeDarkLogo: boolean; 580 | lightBackgroundColor: string; 581 | darkBackgroundColor?: string; 582 | }) => { 583 | const logWrite = ( 584 | emoji: string, 585 | filePath: string, 586 | dimensions?: { width: number; height: number }, 587 | ) => logWriteGlobal(emoji, filePath, workingPath, dimensions); 588 | 589 | const imagesPath = path.resolve(projectPath, "Images.xcassets"); 590 | if (fs.existsSync(imagesPath)) { 591 | const imageSetPath = path.resolve(imagesPath, logoAssetName + ".imageset"); 592 | fs.ensureDirSync(imageSetPath); 593 | 594 | fs.writeFileSync( 595 | path.resolve(imageSetPath, "Contents.json"), 596 | getLogoContentsJson(includeDarkLogo), 597 | "utf-8", 598 | ); 599 | logWrite("✨", path.resolve(imageSetPath, "Contents.json")); 600 | 601 | const colorSetPath = path.resolve(imagesPath, colorAssetName + ".colorset"); 602 | fs.ensureDirSync(colorSetPath); 603 | 604 | fs.writeFileSync( 605 | path.resolve(colorSetPath, "Contents.json"), 606 | getColorsContentsJson(lightBackgroundColor, darkBackgroundColor), 607 | "utf-8", 608 | ); 609 | logWrite("✨", path.resolve(colorSetPath, "Contents.json")); 610 | } 611 | }; 612 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-native", 4 | "target": "ES2015", 5 | "lib": ["ES2015"], 6 | "moduleResolution": "Node", 7 | 8 | "allowJs": false, 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "isolatedModules": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true 14 | }, 15 | "include": ["src/**/*.ts", "src/**/*.tsx"], 16 | "exclude": ["node_modules"] 17 | } 18 | --------------------------------------------------------------------------------